diff --git a/CLAUDE.md b/CLAUDE.md index bf9a21d26d..ad5c9dc2d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,11 +30,11 @@ A `Makefile` wraps most common tasks — run `make help` for the full list. Pref - `flutter test test/disk_test.dart` - Run specific test file (or `make test-one TEST=test/disk_test.dart`) - `cargo test --workspace` - Run all Rust tests (parser, FFI shell, monitor) - SSH e2e (opt-in): set `SBM_E2E_SSH_HOST=` in the workspace-root `.env`, then `cargo test -p sbm_parser --test ssh_e2e` — uploads the generated script to the remote, runs it, and compares the parsed result against direct command output; silently skipped when unset -- A widget test whose tree writes to a store opens the database **in memory**: `SqliteDb.openInMemory()` in `setUp`, `SqliteDb.close` in `tearDown`, and the store's `forTest()` constructor (which only differs by using a distinct store name). Widgets that persist on their own — the floating Agent writes its mode on every change, panes write their width on every drag — write without the test asking them to, so this applies to more trees than it looks like. +- A widget test whose tree writes to a store opens the database **in memory**: `await openTestDb()` from `test/helpers/test_db.dart` in `setUp`, `SqliteDb.close` in `tearDown`, and the store's `forTest()` constructor. That helper is `openInMemory` + the `foreign_keys` pragma + `createTables`; a store backed by a table finds nothing to read without it. `forTest()` no longer varies the name — the schema fixes it — and exists only so the singleton's list cache is not shared between tests. Widgets that persist on their own — the floating Agent writes its mode on every change, panes write their width on every drag — write without the test asking them to, so this applies to more trees than it looks like. - This is also why: a `testWidgets` body runs in a fake-async zone, and a real *file* write started there completes on a callback that zone is no longer pumping. Under Hive that left the box's write lock held, `close()` in `tearDown` blocked forever, and `flutter test` waited on the process — one such test hung the whole run with no failure and no output naming the file. An in-memory database has no such lock, but keeping the writes off disk is still the rule. - **A storage migration keeps a permanent regression test, fed by bytes the release being migrated *from* actually wrote.** Permanent meaning the test is never retired once the migration ships; it runs and finishes on every `flutter test` like any other. It gets one pass over a user's records and is not repeatable — once the marker is written the old data is never read again — so a bug there is silence, not a crash. Seeding through the current adapters proves only that today's code agrees with itself. - - `test/fixtures/hive_v1466/` holds boxes generated by v1.0.1466's own adapters (see its README for the worktree recipe); `test/hive_v1466_migration_test.dart` reads them, `test/hive_import_test.dart` covers the import's own logic (retry, idempotency, per-box progress). - - This is not theoretical. `hive_import_test.dart` seeded `Spi` through the current adapter — typeId 15, SSH nested — while 1466 wrote typeId 3 flat, so the `SpiLegacyAdapter` → `_toSpi` path every upgrading install takes had no coverage. The fixture's first run then exposed `PortForwardConfig` being lost **during import** — the one freezed model with no `.g.dart` and so no generated `toJson`, which `SqliteStore.set` reports by returning `false` rather than throwing. `test/port_forward_store_test.dart` establishes separately that an ordinary save loses it the same way, with no migration involved. + - `test/fixtures/hive_v{1466,1480,1491}/` hold boxes generated by those releases' own adapters (see the shared README for the worktree recipe). `test/hive_release_migration_test.dart` runs **both** steps against each — `HiveImport` and then `KvToTablesMigration` — because the shape between them is one no build ships, and asserting there would prove nothing. `test/hive_import_test.dart` keeps its own scope: the import's mechanics (retry, idempotency, per-box progress), asserted against `kv`, which is all the import produces. + - This is not theoretical, twice over. The fixtures' first run exposed `PortForwardConfig` being lost during import — the one freezed model with no `.g.dart` and so no generated `toJson`, which `SqliteStore.set` reports by returning `false` rather than throwing. Their first run against m004 then exposed four field-name mismatches, each silently dropping a whole store: `pubKeyId` vs `keyId`, `private_key` vs `key`, snake_case in `AgentConversation.toJson`, and generated Hive adapters that could no longer open a box at all. - Do the same for the next migration, and never regenerate a fixture to make a failing test pass. - Size the view, not the surface, when a test depends on a breakpoint: `tester.view.physicalSize` + `devicePixelRatio`. `setSurfaceSize` changes what the tree is laid out in but not what `MediaQuery` reports, so a "phone" test written that way silently exercises the desktop rendering. - `pumpAndSettle` is not usable on a tree containing a text field or another always-scheduling widget: it waits for a frame in which nothing is scheduled, and then gives up after its 10-minute default. Count the frames out with `pump(duration)` instead. `--timeout 30s` keeps any such mistake from costing ten minutes. @@ -79,7 +79,7 @@ This is a Flutter application for managing Linux servers with the following key - `store/` - Local storage implementations over `SqliteStore` (fl_lib) - `lib/view/` - UI layer with pages and widgets - `lib/generated/` - Generated localization files -- `lib/hive/` - Hive adapters, kept only so `HiveImport` can read an upgrading install's old boxes. Nothing writes Hive; the whole directory goes when that import does (TODO in the code) +- `lib/hive/` - Hive adapters, kept only so `HiveImport` can read an upgrading install's old boxes. Nothing writes Hive; the whole directory goes when that import does (TODO in the code). `legacy_adapters.dart` holds the types frozen at what a release wrote — see the storage notes below before adding a model back to `@GenerateAdapters` - `lib/src/rust/` - Generated FRB bindings (do not edit) - `packages/` - Vendored Dart forks referenced by path from pubspec (dartssh2, xterm, fl_lib, fl_build, etc.), each a submodule. The exception is `packages/webui`, an in-repo Svelte package (`@serverbox/webui`) of shared UI primitives and design tokens, consumed as a `file:` dependency by both `monitor/frontend` and `website/` - `third_party/ish-arm64` - The iOS Linux engine, a submodule of the `lollipopkit/ShellBox` fork. Not in `packages/` because it is C built by meson and consumed by the Xcode project rather than by pubspec. Which revision builds is the gitlink, not a hash in a script: move it with `git submodule update --remote third_party/ish-arm64` and `git add`. `scripts/build-ish-ios.sh` builds it out of tree into `build/ish/build-/`, so a build never leaves the submodule dirty @@ -179,12 +179,19 @@ ends, or restore a consumer. - AGAIN, NEVER run code formatting commands. - USE dependency injection via GetIt for services like Stores, Services and etc. - Generate all l10n files using `flutter gen-l10n` command after modifying ARB files. -- Storage is SQLite, not Hive. `Store` in fl_lib is `sealed`, so a new backend has to be added there as another `part of 'iface.dart'`, not in this repo. - - Most stores are rows in one shared `kv(store, key, value, updated_at)` table, with `value` as JSON — so `get` returns what `jsonDecode` produced and the `fromObj` hook rebuilds the model. `connection_stats` and `agent_conversation` own real tables instead, because every read of them is a range over one server. - - Enums are stored **by name**, never by index: an index silently changes meaning when a case is inserted, and these values outlive the build that wrote them. - - A write that should not count as a user edit — a migration flag, a restore, device-local bookkeeping — passes `updateLastUpdateTsOnSet: false`. `Stores.lastModTime` is read off those timestamps and decides which side of a sync wins. - - **A value passed to `SqliteStore.set` needs a `toJson`.** That is the kv-backed stores. `set` encodes with `(value as dynamic).toJson()` and answers `false` on failure instead of throwing, so a model without one is dropped silently on every write. Freezed generates it only when the model has a `.g.dart`; `PortForwardConfig` has a hand-written `fromJson` and no `.g.dart`, so its `toJson` is hand-written too and has to be kept in step with it. Under Hive this could not happen — the generated typeId adapter did the encoding. - - The two table-backed stores do not go through `set` and so do not share the requirement in the same form. `connection_stats` writes scalar columns only (`timestamp` as millis, `result` as the enum name) and needs no `toJson` at all; `agent_conversation` writes `json.encode(conversation.toJson())` into its `data` column, so it does need one, but calls it itself rather than relying on `set`. +- Storage is one encrypted SQLite file, not Hive. `KvStore` in fl_lib is `sealed`, so a new key-value backend has to be added there as another `part of 'iface.dart'`, not in this repo. + - **Two shapes, and which one a store uses is a decision.** `setting` and `history` are rows in the shared `kv(store, key, value, updated_at)` table with `value` as JSON — a hundred unrelated preferences with nothing that queries by field, where adding one should stay a one-line change. Everything with relations owns a table: `server`, `private_key`, `snippet`, `port_forward`, `conn_stat`, `agent_conversation` and the child tables hanging off them. + - **Drift owns the DDL and only the DDL** (`lib/data/store/db.dart`). Queries are hand-written and synchronous, because the UI reads a store while building; Drift's API is async and converting ~100 call sites would be the change, not the schema. Drift never opens the connection either — `SqliteDb` does, applies the cipher and the `foreign_keys` pragma (which is *per connection*), and `createTables` hands the live handle over. + - `AppDb.schemaVersion` is 1 and stays there. Version is `SchemaVersion`, because the steps that matter read Hive boxes and remap ids — outside what a Drift migration can say. Two mechanisms advancing one number is the ambiguity this arrangement removes. + - **A primary key is an id, never something the user typed.** A private key's id *was* its name, so renaming one detached every server pointing at it; a snippet was keyed by its name. Both have generated ids and a `UNIQUE` name column now, so a rename is an `UPDATE` — and a `DuplicateNameException` out of `EntityStore.put` is where a collision is found, rather than in whichever dialog last remembered to check. + - **A list or map field is a child table**, not a JSON array in a column: `server_tag`, `server_env`, `server_jump`, `snippet_tag`, `container_host`. A child carries no sync columns and moves with its parent, so editing one stamps the parent — `ContainerStore` writes through `Stores.server.synced.stamp`. A child table's rows come back ordered by its primary key, so the order the user added tags in is not kept. + - `Tables.syncRoots` is what sync moves as a unit. Each root has `updated_at` and `rev` — two edits in one millisecond are indistinguishable by the clock alone — and a delete writes a `tombstone` row, without which a peer reads the absence as an addition and puts the record back. `conn_stat` and `agent_conversation` are deliberately not roots: connecting is not an edit, and a conversation carries terminal output and reasoning. + - Enums are stored **by name**, never by index: an index silently changes meaning when a case is inserted, and these values outlive the build that wrote them. A migration reading an older record has to translate explicitly — `ConnectionStat`'s `@JsonValue`s are snake_case while the column holds the enum's `name`, and three of the five differ. + - A write that should not count as a user edit — a migration flag, a restore, device-local bookkeeping — passes `updateLastUpdateTsOnSet: false` for a kv store, or an explicit `at:` to `SyncedTable.stamp`. `Stores.lastModTime` is read off those timestamps and decides which side of a sync wins. + - **A value passed to `SqliteStore.set` needs a `toJson`.** That is the kv-backed stores. `set` encodes with `(value as dynamic).toJson()` and answers `false` on failure instead of throwing, so a model without one is dropped silently on every write. Freezed generates it only when the model has a `.g.dart`; `PortForwardConfig` has a hand-written `fromJson` and no `.g.dart`, so its `toJson` is hand-written too and has to be kept in step with it. + - The entity stores write columns and do not go through `set`, so they do not share the requirement in that form. They do need `toJson`/`fromJson` for the backup, which `EntityStore` declares. + - **`INSERT OR REPLACE` is wrong on a row with sync columns or children.** It deletes and reinserts, so every column the statement does not name goes back to its default — `rev` to 0, defeating the one thing it exists for — and every `ON DELETE CASCADE` fires, taking the children with it. Use `EntityStore.upsert`, which is `ON CONFLICT DO UPDATE` naming only the data columns. + - **Changing a model that `lib/hive/` still has an adapter for breaks the import.** The generator emits `fields[n] as String` for a non-nullable field, so a box written before that field existed fails to *open* and its whole store is silently left behind. `Snippet` and `PrivateKeyInfo` are frozen types in `lib/hive/legacy_adapters.dart` for this reason and are out of `@GenerateAdapters`; do the same rather than regenerating. `test/hive_release_migration_test.dart` is what catches it. - USE widgets and utilities from `fl_lib` package for common functionalities. - Such as `CustomAppBar`, `context.showRoundDialog`, `Input`, `Btnx.cancelOk`, etc. - You can use context7 MCP to search `lppcg fl_lib KEYWORD` to find relevant widgets and utilities. diff --git a/docs/src/content/docs/development/architecture.md b/docs/src/content/docs/development/architecture.md index 4b30c709db..088f6130c8 100644 --- a/docs/src/content/docs/development/architecture.md +++ b/docs/src/content/docs/development/architecture.md @@ -44,12 +44,65 @@ Server Box follows clean architecture principles with clear separation between d - Built-in JSON serialization - CopyWith extensions for updates -### Local Storage: Hive - -- **hive_ce**: Community edition of Hive -- Follow the existing model pattern: most stores use `hive_ce`, while some tracked models still declare `@HiveType` and `@HiveField` explicitly -- Type adapters auto-generated -- Persistent key-value storage +### Local Storage: SQLite + +One encrypted file, `store.db`, opened through `package:sqlite3` with the +`sqlite3mc` cipher applied before anything reads it. There are two shapes in +it, and which one a store uses is a decision about whether its records have +relations: + +- **`kv(store, key, value, updated_at)`** holds the settings and the history — + a hundred unrelated preferences with nothing that queries by field, where + adding one should stay a one-line change. `value` is JSON, so a value written + here needs a `toJson`; `SqliteStore.set` answers `false` rather than throwing + when it has none. +- **Entity tables** hold the servers, private keys, snippets, port forwards, + connection statistics and agent conversations. Real columns, foreign keys, + `CHECK` constraints and indexes. + +Drift owns the DDL (`lib/data/store/db.dart`), and only the DDL: the app's +queries are hand-written and synchronous, because the UI reads a store while +building. Drift never opens the connection — `SqliteDb` does, applies the +cipher and the `foreign_keys` pragma, and hands the live handle over. + +Two conventions the key-value layout could not hold: + +- **A primary key is an id, never something the user typed.** Renaming a + private key used to detach every server pointing at it, because the key's id + *was* its name. Names are ordinary `UNIQUE` columns now. +- **A list or map field is a child table.** That makes "every server with this + tag" a query rather than a decode of every row, and lets `ON DELETE CASCADE` + clean up after a deleted server instead of six hand-written calls. + +`Tables.syncRoots` names the tables that are a unit of sync. Each carries +`updated_at` and `rev`; their children carry neither and move with the parent, +so a tag cannot arrive before the server it belongs to. A delete leaves a row +in `tombstone`, without which a peer reads the record's absence as an addition +and puts it back. + +### Storage migrations + +`SchemaVersion` tracks the layout; Drift's own `schemaVersion` is pinned at 1 +and stays there, because the steps that matter are outside what a Drift +migration can express. Two of them exist: + +- `HiveImport` (m003) copies an upgrading install's Hive boxes into `kv`, once + per device. It reads through frozen adapters in + `lib/hive/legacy_adapters.dart` rather than through the live models — adding + a field to a model makes a *generated* adapter unable to read any box written + before it. +- `KvToTablesMigration` (m004) takes those rows apart into the entity tables, + generating ids for the records that were keyed by name and rewriting every + reference to them. + +**A storage migration keeps a permanent regression test, fed by bytes the +release being migrated *from* actually wrote.** It gets one pass over a user's +records and is not repeatable, so a bug there is silence rather than a crash. +`test/fixtures/hive_v{1466,1480,1491}/` hold boxes produced by those releases' +own adapters, and `test/hive_release_migration_test.dart` runs both steps +against each. Seeding through the current adapters would only show that today's +code agrees with itself — on its first run that test found four field-name +mismatches, each of which silently dropped an entire store. ## Dependency Injection diff --git a/docs/src/content/docs/zh/development/architecture.md b/docs/src/content/docs/zh/development/architecture.md index 4209c61078..5b8023d4da 100644 --- a/docs/src/content/docs/zh/development/architecture.md +++ b/docs/src/content/docs/zh/development/architecture.md @@ -44,12 +44,52 @@ Server Box 遵循整洁架构 (Clean Architecture) 原则,在数据层、领 - 内置 JSON 序列化支持 - 提供 CopyWith 扩展以便于更新 -### 本地存储:Hive - -- **hive_ce**:Hive 的社区版 -- 遵循现有模型模式:多数存储使用 `hive_ce`,部分已跟踪模型仍显式声明 `@HiveType` 和 `@HiveField` -- 类型适配器 (Type adapters) 自动生成 -- 持久化键值对存储 +### 本地存储:SQLite + +一个加密文件 `store.db`,经 `package:sqlite3` 打开,读取任何数据之前先应用 +`sqlite3mc` 加密。其中有两种形态,某个 store 用哪一种,取决于它的记录之间 +是否存在关系: + +- **`kv(store, key, value, updated_at)`** 存放 settings 和 history —— 上百条 + 互不相关的偏好项,没有任何按字段的查询,新增一项应当只是一行改动。`value` + 是 JSON,因此写入这里的值必须有 `toJson`;缺少时 `SqliteStore.set` 返回 + `false` 而不是抛出。 +- **实体表**存放服务器、私钥、snippet、端口转发、连接统计和 agent + conversation。真正的列、外键、`CHECK` 约束和索引。 + +Drift 负责 DDL(`lib/data/store/db.dart`),且仅负责 DDL:查询是手写且同步的, +因为 UI 在 build 过程中读取 store。Drift 从不打开连接 —— 由 `SqliteDb` 打开、 +应用加密和 `foreign_keys` pragma,再把已打开的 handle 交给它。 + +两条键值形态无法表达的约定: + +- **主键只能是 id,不能是用户输入的名字。** 过去重命名私钥会让所有引用它的 + 服务器失联,因为私钥的 id 就是它的名字。现在 name 是普通的 `UNIQUE` 列。 +- **列表或 map 字段是子表。** 这让“带此 tag 的所有服务器”成为一次查询,而不是 + 解码每一行;也让 `ON DELETE CASCADE` 接手删除服务器后的清理,取代原先六处 + 手写调用。 + +`Tables.syncRoots` 列出作为同步单元的表。每张都带 `updated_at` 和 `rev`; +它们的子表两者都不带,随父记录一起移动,因此 tag 不会先于它所属的服务器到达。 +删除会在 `tombstone` 留下一行,否则对端会把记录的缺失读成新增并将其写回。 + +### 存储迁移 + +`SchemaVersion` 跟踪布局版本;Drift 自身的 `schemaVersion` 固定为 1 且不再变化, +因为真正重要的步骤超出 Drift migration 的表达能力。目前有两步: + +- `HiveImport`(m003)把升级安装的 Hive box 复制进 `kv`,每台设备一次。它通过 + `lib/hive/legacy_adapters.dart` 中冻结的 adapter 读取,而不是通过当前模型 —— + 给模型新增字段会让*生成的* adapter 无法读取此前写入的任何 box。 +- `KvToTablesMigration`(m004)把这些行拆进实体表,为原本以 name 为键的记录 + 生成 id,并重写指向它们的每一处引用。 + +**存储迁移必须保留一个永久回归测试,输入是被迁移版本真实写出的字节。** +它对用户数据只有一次机会且不可重复,因此其中的 bug 表现为静默而非崩溃。 +`test/fixtures/hive_v{1466,1480,1491}/` 保存了这些版本各自 adapter 生成的 box, +`test/hive_release_migration_test.dart` 对每个版本跑完这两步。用当前 adapter +造数据只能证明今天的代码自洽 —— 该测试首次运行就发现了四处字段名不匹配, +每一处都会静默丢掉一整个 store。 ## 依赖注入 (DI) diff --git a/lib/core/sync.dart b/lib/core/sync.dart index f0d9216307..0b3105cff5 100644 --- a/lib/core/sync.dart +++ b/lib/core/sync.dart @@ -25,7 +25,7 @@ final class BakSyncer extends SyncIface { /// didn't understand. [backup] is the one hook available for refusing that /// without forking the whole cycle. /// - /// Static because the syncer is a const singleton. + /// Static because the syncer is a single instance shared by every caller. static SchemaTooNewException? _remoteTooNew; /// Whether the last sync attempt aborted because the remote data is newer diff --git a/lib/data/model/app/bak/backup.dart b/lib/data/model/app/bak/backup.dart index 33165d8e2f..c4de60ce97 100644 --- a/lib/data/model/app/bak/backup.dart +++ b/lib/data/model/app/bak/backup.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:io'; import 'package:fl_lib/fl_lib.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -7,15 +6,18 @@ import 'package:logging/logging.dart'; import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/snippet.dart'; -import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/store.dart'; part 'backup.g.dart'; -const backupFormatVersion = 1; - final _logger = Logger('Backup'); +/// The first backup format, kept for reading only. +/// +/// The app writes [BackupV2]; this exists so a file from a build old enough to +/// have written it can still be restored. It carries one timestamp for the +/// whole file rather than one per record, which is why a merge here can only +/// take or leave each store whole. @JsonSerializable() class Backup implements Mergeable { // backup format version @@ -45,29 +47,6 @@ class Backup implements Mergeable { Map toJson() => _$BackupToJson(this); - static Future loadFromStore() async { - final lastModTime = Stores.lastModTime; - return Backup( - version: backupFormatVersion, - date: DateTime.now().toString().split('.').firstOrNull ?? '', - spis: Stores.server.fetch(), - snippets: Stores.snippet.fetch(), - keys: Stores.key.fetch(), - container: Stores.container.getAllMap(), - lastModTime: lastModTime, - history: Stores.history.getAllMap(), - settings: Stores.setting.getAllMap(), - ); - } - - static Future backup([String? name]) async { - final bak = await Backup.loadFromStore(); - final result = _diyEncrypt(json.encode(bak.toJson())); - final path = Paths.doc.joinPath(name ?? Miscs.bakFileName); - await File(path).writeAsString(result); - return path; - } - @override Future merge({bool force = false}) async { final curTime = Stores.lastModTime; @@ -82,16 +61,17 @@ class Backup implements Mergeable { // restore that was interrupted — process killed, device out of battery — // with servers deleted whose replacements were never written, and no way to // tell that had happened. + // + // Whole stores rather than per record: this format carries one timestamp + // for the entire file, so there is nothing to compare a single record + // against. Ordered by what references what — a server names a private key, + // and a snippet names a server. SqliteStore.transact(() { - _restoreInto( - Stores.snippet, - {for (final s in snippets) s.name: s}, - force: force, - ); - _restoreInto(Stores.server, {for (final s in spis) s.id: s}, force: force); - _restoreInto(Stores.key, {for (final s in keys) s.id: s}, force: force); + Stores.key.replaceAll(keys); + Stores.server.replaceAll(spis); + Stores.snippet.replaceAll(snippets); + Stores.container.restoreLegacyMap(container); _restoreInto(Stores.history, history, force: force); - _restoreInto(Stores.container, container, force: force); final settings_ = settings; if (settings_ != null) { @@ -109,7 +89,7 @@ class Backup implements Mergeable { Backup.fromJson(json.decode(_diyDecrypt(raw))); } -/// Writes one section of a backup into the store it came from. +/// Writes one section of a backup into the key-value store it came from. /// /// [force] replaces what is there; otherwise the store is also made to *stop* /// holding whatever the backup does not, which is what makes a delete on one @@ -144,9 +124,6 @@ void _restoreInto( } } -String _diyEncrypt(String raw) => - json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false)); - String _diyDecrypt(String raw) { try { final list = json.decode(raw); diff --git a/lib/data/model/app/bak/backup2.dart b/lib/data/model/app/bak/backup2.dart index 98266e5308..ccf7240daf 100644 --- a/lib/data/model/app/bak/backup2.dart +++ b/lib/data/model/app/bak/backup2.dart @@ -6,6 +6,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:logging/logging.dart'; import 'package:server_box/data/model/server/custom.dart'; import 'package:server_box/data/model/server/monitor_http_credential.dart'; +import 'package:server_box/data/model/server/port_forward.dart'; import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/snippet.dart'; @@ -47,6 +48,11 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable { required Map container, required Map history, required Map settings, + + /// Absent from every file written before port forwards became a record of + /// their own, so it defaults rather than being required — an older backup + /// has to keep decoding. + @Default({}) Map portForwards, }) = _BackupV2; /// Must stay a single expression with a cascade, not a block body. @@ -64,23 +70,20 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable { _validateRestorableTypedStores(); _loggerV2.info('Merging...'); - final results = await Future.wait([ - Mergeable.mergeStore( - backupData: spis, - store: Stores.server, - force: force, - ), - Mergeable.mergeStore( - backupData: snippets, - store: Stores.snippet, - force: force, - ), - Mergeable.mergeStore(backupData: keys, store: Stores.key, force: force), - Mergeable.mergeStore( - backupData: container, - store: Stores.container, - force: force, - ), + // Ordered by what references what: a server names a private key, a snippet + // and a port forward name a server, and a container host is a child of one. + // Merging a store before the one it points at would drop every record whose + // foreign key has not arrived yet. + final keysChanged = Stores.key.merge(keys, force: force); + final serversChanged = Stores.server.merge(spis, force: force); + final snippetsChanged = Stores.snippet.merge(snippets, force: force); + Stores.portForward.merge(portForwards, force: force); + for (final entry in container.entries) { + if (entry.key.startsWith(StoreDefaults.prefixKey)) continue; + Stores.container.restoreOne(entry.key, entry.value); + } + + await Future.wait([ Mergeable.mergeStore( backupData: history, store: Stores.history, @@ -91,14 +94,14 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable { backupData: settings, store: Stores.setting, force: force, - ) - else - Future.value(false), + ), ]); - if (results[0]) GlobalRef.gRef?.read(serversProvider.notifier).reload(); - if (results[1]) GlobalRef.gRef?.read(snippetProvider.notifier).reload(); - if (results[2]) GlobalRef.gRef?.read(privateKeyProvider.notifier).reload(); + if (serversChanged) GlobalRef.gRef?.read(serversProvider.notifier).reload(); + if (snippetsChanged) { + GlobalRef.gRef?.read(snippetProvider.notifier).reload(); + } + if (keysChanged) GlobalRef.gRef?.read(privateKeyProvider.notifier).reload(); _loggerV2.info('Merge completed'); } @@ -115,10 +118,11 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable { return BackupV2( version: formatVer, date: DateTimeX.timestamp, - spis: Stores.server.getAllMap(includeInternalKeys: true), - snippets: Stores.snippet.getAllMap(includeInternalKeys: true), - keys: Stores.key.getAllMap(includeInternalKeys: true), - container: Stores.container.getAllMap(includeInternalKeys: true), + spis: Stores.server.getAllMap(), + snippets: Stores.snippet.getAllMap(), + keys: Stores.key.getAllMap(), + portForwards: Stores.portForward.getAllMap(), + container: Stores.container.getAllMap(), history: Stores.history.getAllMap(includeInternalKeys: true), settings: includeSettings ? Stores.setting.getAllMap(includeInternalKeys: true) @@ -170,6 +174,7 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable { _validateRestorableStore('spis', spis); _validateRestorableStore('snippets', snippets); _validateRestorableStore('keys', keys); + _validateRestorableStore('portForwards', portForwards); } } @@ -180,6 +185,7 @@ Object? _toEncodable(Object? value) { final Spi spi => spi.toJson(), final Snippet snippet => snippet.toJson(), final PrivateKeyInfo key => key.toJson(), + final PortForwardConfig forward => forward.toJson(), final ServerCustom custom => custom.toJson(), final WakeOnLanCfg wolCfg => wolCfg.toJson(), // Nested on Spi. Both were missing, so backing up a server that used diff --git a/lib/data/model/app/bak/backup2.freezed.dart b/lib/data/model/app/bak/backup2.freezed.dart index 622e047977..8551a89914 100644 --- a/lib/data/model/app/bak/backup2.freezed.dart +++ b/lib/data/model/app/bak/backup2.freezed.dart @@ -15,7 +15,10 @@ T _$identity(T value) => value; /// @nodoc mixin _$BackupV2 { - int get version; int get date; Map get spis; Map get snippets; Map get keys; Map get container; Map get history; Map get settings; + int get version; int get date; Map get spis; Map get snippets; Map get keys; Map get container; Map get history; Map get settings;/// Absent from every file written before port forwards became a record of +/// their own, so it defaults rather than being required — an older backup +/// has to keep decoding. + Map get portForwards; /// Create a copy of BackupV2 /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +31,16 @@ $BackupV2CopyWith get copyWith => _$BackupV2CopyWithImpl(thi @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other.spis, spis)&&const DeepCollectionEquality().equals(other.snippets, snippets)&&const DeepCollectionEquality().equals(other.keys, keys)&&const DeepCollectionEquality().equals(other.container, container)&&const DeepCollectionEquality().equals(other.history, history)&&const DeepCollectionEquality().equals(other.settings, settings)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other.spis, spis)&&const DeepCollectionEquality().equals(other.snippets, snippets)&&const DeepCollectionEquality().equals(other.keys, keys)&&const DeepCollectionEquality().equals(other.container, container)&&const DeepCollectionEquality().equals(other.history, history)&&const DeepCollectionEquality().equals(other.settings, settings)&&const DeepCollectionEquality().equals(other.portForwards, portForwards)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(spis),const DeepCollectionEquality().hash(snippets),const DeepCollectionEquality().hash(keys),const DeepCollectionEquality().hash(container),const DeepCollectionEquality().hash(history),const DeepCollectionEquality().hash(settings)); +int get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(spis),const DeepCollectionEquality().hash(snippets),const DeepCollectionEquality().hash(keys),const DeepCollectionEquality().hash(container),const DeepCollectionEquality().hash(history),const DeepCollectionEquality().hash(settings),const DeepCollectionEquality().hash(portForwards)); @override String toString() { - return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)'; + return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings, portForwards: $portForwards)'; } @@ -48,7 +51,7 @@ abstract mixin class $BackupV2CopyWith<$Res> { factory $BackupV2CopyWith(BackupV2 value, $Res Function(BackupV2) _then) = _$BackupV2CopyWithImpl; @useResult $Res call({ - int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings + int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings, Map portForwards }); @@ -65,7 +68,7 @@ class _$BackupV2CopyWithImpl<$Res> /// Create a copy of BackupV2 /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,Object? portForwards = null,}) { return _then(_self.copyWith( version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable as int,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable @@ -75,6 +78,7 @@ as Map,keys: null == keys ? _self.keys : keys // ignore: cast_n as Map,container: null == container ? _self.container : container // ignore: cast_nullable_to_non_nullable as Map,history: null == history ? _self.history : history // ignore: cast_nullable_to_non_nullable as Map,settings: null == settings ? _self.settings : settings // ignore: cast_nullable_to_non_nullable +as Map,portForwards: null == portForwards ? _self.portForwards : portForwards // ignore: cast_nullable_to_non_nullable as Map, )); } @@ -160,10 +164,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings, Map portForwards)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _BackupV2() when $default != null: -return $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_that.container,_that.history,_that.settings);case _: +return $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_that.container,_that.history,_that.settings,_that.portForwards);case _: return orElse(); } @@ -181,10 +185,10 @@ return $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_t /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings, Map portForwards) $default,) {final _that = this; switch (_that) { case _BackupV2(): -return $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_that.container,_that.history,_that.settings);case _: +return $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_that.container,_that.history,_that.settings,_that.portForwards);case _: throw StateError('Unexpected subclass'); } @@ -201,10 +205,10 @@ return $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_t /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings, Map portForwards)? $default,) {final _that = this; switch (_that) { case _BackupV2() when $default != null: -return $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_that.container,_that.history,_that.settings);case _: +return $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_that.container,_that.history,_that.settings,_that.portForwards);case _: return null; } @@ -216,7 +220,7 @@ return $default(_that.version,_that.date,_that.spis,_that.snippets,_that.keys,_t @JsonSerializable() class _BackupV2 extends BackupV2 { - const _BackupV2({required this.version, required this.date, required final Map spis, required final Map snippets, required final Map keys, required final Map container, required final Map history, required final Map settings}): _spis = spis,_snippets = snippets,_keys = keys,_container = container,_history = history,_settings = settings,super._(); + const _BackupV2({required this.version, required this.date, required final Map spis, required final Map snippets, required final Map keys, required final Map container, required final Map history, required final Map settings, final Map portForwards = const {}}): _spis = spis,_snippets = snippets,_keys = keys,_container = container,_history = history,_settings = settings,_portForwards = portForwards,super._(); factory _BackupV2.fromJson(Map json) => _$BackupV2FromJson(json); @override final int version; @@ -263,6 +267,19 @@ class _BackupV2 extends BackupV2 { return EqualUnmodifiableMapView(_settings); } +/// Absent from every file written before port forwards became a record of +/// their own, so it defaults rather than being required — an older backup +/// has to keep decoding. + final Map _portForwards; +/// Absent from every file written before port forwards became a record of +/// their own, so it defaults rather than being required — an older backup +/// has to keep decoding. +@override@JsonKey() Map get portForwards { + if (_portForwards is EqualUnmodifiableMapView) return _portForwards; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_portForwards); +} + /// Create a copy of BackupV2 /// with the given fields replaced by the non-null parameter values. @@ -277,16 +294,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other._spis, _spis)&&const DeepCollectionEquality().equals(other._snippets, _snippets)&&const DeepCollectionEquality().equals(other._keys, _keys)&&const DeepCollectionEquality().equals(other._container, _container)&&const DeepCollectionEquality().equals(other._history, _history)&&const DeepCollectionEquality().equals(other._settings, _settings)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other._spis, _spis)&&const DeepCollectionEquality().equals(other._snippets, _snippets)&&const DeepCollectionEquality().equals(other._keys, _keys)&&const DeepCollectionEquality().equals(other._container, _container)&&const DeepCollectionEquality().equals(other._history, _history)&&const DeepCollectionEquality().equals(other._settings, _settings)&&const DeepCollectionEquality().equals(other._portForwards, _portForwards)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(_spis),const DeepCollectionEquality().hash(_snippets),const DeepCollectionEquality().hash(_keys),const DeepCollectionEquality().hash(_container),const DeepCollectionEquality().hash(_history),const DeepCollectionEquality().hash(_settings)); +int get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(_spis),const DeepCollectionEquality().hash(_snippets),const DeepCollectionEquality().hash(_keys),const DeepCollectionEquality().hash(_container),const DeepCollectionEquality().hash(_history),const DeepCollectionEquality().hash(_settings),const DeepCollectionEquality().hash(_portForwards)); @override String toString() { - return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)'; + return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings, portForwards: $portForwards)'; } @@ -297,7 +314,7 @@ abstract mixin class _$BackupV2CopyWith<$Res> implements $BackupV2CopyWith<$Res> factory _$BackupV2CopyWith(_BackupV2 value, $Res Function(_BackupV2) _then) = __$BackupV2CopyWithImpl; @override @useResult $Res call({ - int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings + int version, int date, Map spis, Map snippets, Map keys, Map container, Map history, Map settings, Map portForwards }); @@ -314,7 +331,7 @@ class __$BackupV2CopyWithImpl<$Res> /// Create a copy of BackupV2 /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,Object? portForwards = null,}) { return _then(_BackupV2( version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable as int,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable @@ -324,6 +341,7 @@ as Map,keys: null == keys ? _self._keys : keys // ignore: cast_ as Map,container: null == container ? _self._container : container // ignore: cast_nullable_to_non_nullable as Map,history: null == history ? _self._history : history // ignore: cast_nullable_to_non_nullable as Map,settings: null == settings ? _self._settings : settings // ignore: cast_nullable_to_non_nullable +as Map,portForwards: null == portForwards ? _self._portForwards : portForwards // ignore: cast_nullable_to_non_nullable as Map, )); } diff --git a/lib/data/model/app/bak/backup2.g.dart b/lib/data/model/app/bak/backup2.g.dart index c1f1b3fe57..20d4b5a60a 100644 --- a/lib/data/model/app/bak/backup2.g.dart +++ b/lib/data/model/app/bak/backup2.g.dart @@ -15,6 +15,9 @@ _BackupV2 _$BackupV2FromJson(Map json) => _BackupV2( container: json['container'] as Map, history: json['history'] as Map, settings: json['settings'] as Map, + portForwards: + json['portForwards'] as Map? ?? + const {}, ); Map _$BackupV2ToJson(_BackupV2 instance) => { @@ -26,4 +29,5 @@ Map _$BackupV2ToJson(_BackupV2 instance) => { 'container': instance.container, 'history': instance.history, 'settings': instance.settings, + 'portForwards': instance.portForwards, }; diff --git a/lib/data/model/server/private_key_info.dart b/lib/data/model/server/private_key_info.dart index b6f839590e..fd5f26bdd9 100644 --- a/lib/data/model/server/private_key_info.dart +++ b/lib/data/model/server/private_key_info.dart @@ -4,17 +4,43 @@ part 'private_key_info.g.dart'; @JsonSerializable() class PrivateKeyInfo { + /// Generated, and what `SshCredential.keyId` points at. + /// + /// Was the user-typed name until the tables landed, so renaming a key + /// detached every server using it. The two are separate fields now and the + /// reference is to this one. final String id; + + /// What the user typed, and what the list shows. Unique, enforced by the + /// schema rather than by the two pages that can create one. + final String name; + @JsonKey(name: 'private_key') final String key; - const PrivateKeyInfo({required this.id, required this.key}); + const PrivateKeyInfo({ + required this.id, + required this.name, + required this.key, + }); + /// [name] falls back to [id] for a record written before they were separate: + /// back then the id *was* the name, which is exactly what to show. factory PrivateKeyInfo.fromJson(Map json) => - _$PrivateKeyInfoFromJson(json); + _$PrivateKeyInfoFromJson({ + ...json, + if ((json['name'] as String?)?.isNotEmpty != true) 'name': json['id'], + }); Map toJson() => _$PrivateKeyInfoToJson(this); + PrivateKeyInfo copyWith({String? id, String? name, String? key}) => + PrivateKeyInfo( + id: id ?? this.id, + name: name ?? this.name, + key: key ?? this.key, + ); + String? get type { final lines = key.split('\n'); if (lines.length < 2) { diff --git a/lib/data/model/server/private_key_info.g.dart b/lib/data/model/server/private_key_info.g.dart index 2371e84464..20a10cc3d6 100644 --- a/lib/data/model/server/private_key_info.g.dart +++ b/lib/data/model/server/private_key_info.g.dart @@ -9,8 +9,13 @@ part of 'private_key_info.dart'; PrivateKeyInfo _$PrivateKeyInfoFromJson(Map json) => PrivateKeyInfo( id: json['id'] as String, + name: json['name'] as String, key: json['private_key'] as String, ); Map _$PrivateKeyInfoToJson(PrivateKeyInfo instance) => - {'id': instance.id, 'private_key': instance.key}; + { + 'id': instance.id, + 'name': instance.name, + 'private_key': instance.key, + }; diff --git a/lib/data/model/server/snippet.dart b/lib/data/model/server/snippet.dart index 3d44c5ac3c..8e4c24c7dc 100644 --- a/lib/data/model/server/snippet.dart +++ b/lib/data/model/server/snippet.dart @@ -11,6 +11,9 @@ part 'snippet.freezed.dart'; @freezed abstract class Snippet with _$Snippet { const factory Snippet({ + /// Generated. A snippet used to be keyed by [name], so renaming one was a + /// delete and an insert — and `snippetOrder` still pointed at the old name. + required String id, required String name, required String script, List? tags, @@ -20,10 +23,17 @@ abstract class Snippet with _$Snippet { List? autoRunOn, }) = _Snippet; - factory Snippet.fromJson(Map json) => - _$SnippetFromJson(json); + /// A record written before snippets had ids gets one here. + /// + /// Safe to generate rather than derive: nothing references a snippet. Its + /// `autoRunOn` names *servers*, and `snippetOrder` is a list of names. + factory Snippet.fromJson(Map json) => _$SnippetFromJson({ + ...json, + if ((json['id'] as String?)?.isNotEmpty != true) 'id': ShortId.generate(), + }); static const example = Snippet( + id: 'example', name: 'example', script: 'echo hello', tags: ['tag'], diff --git a/lib/data/model/server/snippet.freezed.dart b/lib/data/model/server/snippet.freezed.dart index 4620aca355..df99db4aff 100644 --- a/lib/data/model/server/snippet.freezed.dart +++ b/lib/data/model/server/snippet.freezed.dart @@ -15,7 +15,9 @@ T _$identity(T value) => value; /// @nodoc mixin _$Snippet { - String get name; String get script; List? get tags; String? get note;/// List of server id that this snippet should be auto run on +/// Generated. A snippet used to be keyed by [name], so renaming one was a +/// delete and an insert — and `snippetOrder` still pointed at the old name. + String get id; String get name; String get script; List? get tags; String? get note;/// List of server id that this snippet should be auto run on List? get autoRunOn; /// Create a copy of Snippet /// with the given fields replaced by the non-null parameter values. @@ -29,16 +31,16 @@ $SnippetCopyWith get copyWith => _$SnippetCopyWithImpl(this as @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is Snippet&&(identical(other.name, name) || other.name == name)&&(identical(other.script, script) || other.script == script)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.note, note) || other.note == note)&&const DeepCollectionEquality().equals(other.autoRunOn, autoRunOn)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is Snippet&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.script, script) || other.script == script)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.note, note) || other.note == note)&&const DeepCollectionEquality().equals(other.autoRunOn, autoRunOn)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,name,script,const DeepCollectionEquality().hash(tags),note,const DeepCollectionEquality().hash(autoRunOn)); +int get hashCode => Object.hash(runtimeType,id,name,script,const DeepCollectionEquality().hash(tags),note,const DeepCollectionEquality().hash(autoRunOn)); @override String toString() { - return 'Snippet(name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)'; + return 'Snippet(id: $id, name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)'; } @@ -49,7 +51,7 @@ abstract mixin class $SnippetCopyWith<$Res> { factory $SnippetCopyWith(Snippet value, $Res Function(Snippet) _then) = _$SnippetCopyWithImpl; @useResult $Res call({ - String name, String script, List? tags, String? note, List? autoRunOn + String id, String name, String script, List? tags, String? note, List? autoRunOn }); @@ -66,9 +68,10 @@ class _$SnippetCopyWithImpl<$Res> /// Create a copy of Snippet /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? script = null,Object? tags = freezed,Object? note = freezed,Object? autoRunOn = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? script = null,Object? tags = freezed,Object? note = freezed,Object? autoRunOn = freezed,}) { return _then(_self.copyWith( -name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,script: null == script ? _self.script : script // ignore: cast_nullable_to_non_nullable as String,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable as List?,note: freezed == note ? _self.note : note // ignore: cast_nullable_to_non_nullable @@ -158,10 +161,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String name, String script, List? tags, String? note, List? autoRunOn)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String name, String script, List? tags, String? note, List? autoRunOn)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _Snippet() when $default != null: -return $default(_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);case _: +return $default(_that.id,_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);case _: return orElse(); } @@ -179,10 +182,10 @@ return $default(_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);c /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String name, String script, List? tags, String? note, List? autoRunOn) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String name, String script, List? tags, String? note, List? autoRunOn) $default,) {final _that = this; switch (_that) { case _Snippet(): -return $default(_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);case _: +return $default(_that.id,_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);case _: throw StateError('Unexpected subclass'); } @@ -199,10 +202,10 @@ return $default(_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);c /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String name, String script, List? tags, String? note, List? autoRunOn)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String name, String script, List? tags, String? note, List? autoRunOn)? $default,) {final _that = this; switch (_that) { case _Snippet() when $default != null: -return $default(_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);case _: +return $default(_that.id,_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);case _: return null; } @@ -214,9 +217,12 @@ return $default(_that.name,_that.script,_that.tags,_that.note,_that.autoRunOn);c @JsonSerializable() class _Snippet implements Snippet { - const _Snippet({required this.name, required this.script, final List? tags, this.note, final List? autoRunOn}): _tags = tags,_autoRunOn = autoRunOn; + const _Snippet({required this.id, required this.name, required this.script, final List? tags, this.note, final List? autoRunOn}): _tags = tags,_autoRunOn = autoRunOn; factory _Snippet.fromJson(Map json) => _$SnippetFromJson(json); +/// Generated. A snippet used to be keyed by [name], so renaming one was a +/// delete and an insert — and `snippetOrder` still pointed at the old name. +@override final String id; @override final String name; @override final String script; final List? _tags; @@ -254,16 +260,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _Snippet&&(identical(other.name, name) || other.name == name)&&(identical(other.script, script) || other.script == script)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.note, note) || other.note == note)&&const DeepCollectionEquality().equals(other._autoRunOn, _autoRunOn)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Snippet&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.script, script) || other.script == script)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.note, note) || other.note == note)&&const DeepCollectionEquality().equals(other._autoRunOn, _autoRunOn)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,name,script,const DeepCollectionEquality().hash(_tags),note,const DeepCollectionEquality().hash(_autoRunOn)); +int get hashCode => Object.hash(runtimeType,id,name,script,const DeepCollectionEquality().hash(_tags),note,const DeepCollectionEquality().hash(_autoRunOn)); @override String toString() { - return 'Snippet(name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)'; + return 'Snippet(id: $id, name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)'; } @@ -274,7 +280,7 @@ abstract mixin class _$SnippetCopyWith<$Res> implements $SnippetCopyWith<$Res> { factory _$SnippetCopyWith(_Snippet value, $Res Function(_Snippet) _then) = __$SnippetCopyWithImpl; @override @useResult $Res call({ - String name, String script, List? tags, String? note, List? autoRunOn + String id, String name, String script, List? tags, String? note, List? autoRunOn }); @@ -291,9 +297,10 @@ class __$SnippetCopyWithImpl<$Res> /// Create a copy of Snippet /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? script = null,Object? tags = freezed,Object? note = freezed,Object? autoRunOn = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? script = null,Object? tags = freezed,Object? note = freezed,Object? autoRunOn = freezed,}) { return _then(_Snippet( -name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,script: null == script ? _self.script : script // ignore: cast_nullable_to_non_nullable as String,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable as List?,note: freezed == note ? _self.note : note // ignore: cast_nullable_to_non_nullable diff --git a/lib/data/model/server/snippet.g.dart b/lib/data/model/server/snippet.g.dart index 39c66a638b..c52ceb74e6 100644 --- a/lib/data/model/server/snippet.g.dart +++ b/lib/data/model/server/snippet.g.dart @@ -7,6 +7,7 @@ part of 'snippet.dart'; // ************************************************************************** _Snippet _$SnippetFromJson(Map json) => _Snippet( + id: json['id'] as String, name: json['name'] as String, script: json['script'] as String, tags: (json['tags'] as List?)?.map((e) => e as String).toList(), @@ -17,6 +18,7 @@ _Snippet _$SnippetFromJson(Map json) => _Snippet( ); Map _$SnippetToJson(_Snippet instance) => { + 'id': instance.id, 'name': instance.name, 'script': instance.script, 'tags': instance.tags, diff --git a/lib/data/provider/ai/agent_session.g.dart b/lib/data/provider/ai/agent_session.g.dart index c473e08c0d..fe21db92ea 100644 --- a/lib/data/provider/ai/agent_session.g.dart +++ b/lib/data/provider/ai/agent_session.g.dart @@ -62,7 +62,7 @@ final class AgentSessionProvider } } -String _$agentSessionHash() => r'59ff055bf74e2b4004b3d9788f902977ba64fa3e'; +String _$agentSessionHash() => r'fc8941a55ff3364e9f8381adb6e3e6d0c8ed4fb9'; /// The app-wide Agent conversation, and everything it is doing right now. /// diff --git a/lib/data/provider/port_forward_provider.dart b/lib/data/provider/port_forward_provider.dart index b0e4060088..6926edbff7 100644 --- a/lib/data/provider/port_forward_provider.dart +++ b/lib/data/provider/port_forward_provider.dart @@ -27,7 +27,7 @@ class PortForwardNotifier extends _$PortForwardNotifier { state = state.copyWith(activeForwards: {}); } }); - final configs = Stores.portForward.fetch(serverId); + final configs = Stores.portForward.fetchForServer(serverId); return PortForwardState(serverId: serverId, configs: configs); } diff --git a/lib/data/provider/port_forward_provider.g.dart b/lib/data/provider/port_forward_provider.g.dart index 470e873982..dc3d7ac96f 100644 --- a/lib/data/provider/port_forward_provider.g.dart +++ b/lib/data/provider/port_forward_provider.g.dart @@ -59,7 +59,7 @@ final class PortForwardNotifierProvider } String _$portForwardNotifierHash() => - r'578c114703b404b3fe053d46bb3be2d09d8cd2cb'; + r'b84deef9c769e337f16c490be6fe7a8628a9eab0'; final class PortForwardNotifierFamily extends $Family with diff --git a/lib/data/provider/private_key.dart b/lib/data/provider/private_key.dart index f61df63775..2d3b295538 100644 --- a/lib/data/provider/private_key.dart +++ b/lib/data/provider/private_key.dart @@ -22,7 +22,7 @@ class PrivateKeyNotifier extends _$PrivateKeyNotifier { } void reload() { - Stores.key.invalidateCache(); + Stores.key.dropCache(); final newState = _load(); if (newState == state) return; state = newState; @@ -47,17 +47,23 @@ class PrivateKeyNotifier extends _$PrivateKeyNotifier { bakSync.sync(milliDelay: 1000); } + /// The id never changes, so this is one write either way. The branch that + /// deleted the old record existed because the id *was* the name — and with + /// ids equal it deleted the record it had just written. Future update(PrivateKeyInfo old, PrivateKeyInfo newInfo) async { + if (old.id != newInfo.id) { + // `EntityStore.update` refuses this; going straight to `put` would let a + // caller insert a second record under the name of the first. + throw ArgumentError('cannot change the id of a PrivateKeyInfo'); + } final keys = [...state.keys]; final idx = keys.indexWhere((e) => e.id == old.id); if (idx == -1) { keys.add(newInfo); - Stores.key.put(newInfo); - Stores.key.delete(old); } else { keys[idx] = newInfo; - Stores.key.update(old, newInfo); } + Stores.key.put(newInfo); state = state.copyWith(keys: keys); bakSync.sync(milliDelay: 1000); } diff --git a/lib/data/provider/private_key.g.dart b/lib/data/provider/private_key.g.dart index a3f617dc8c..36950f051b 100644 --- a/lib/data/provider/private_key.g.dart +++ b/lib/data/provider/private_key.g.dart @@ -42,7 +42,7 @@ final class PrivateKeyNotifierProvider } String _$privateKeyNotifierHash() => - r'79d02e116fe665df1ccb0719590947e109a5a736'; + r'63c122451f3533089a277d862311efc1ad5ccec1'; abstract class _$PrivateKeyNotifier extends $Notifier { PrivateKeyState build(); diff --git a/lib/data/provider/server/all.dart b/lib/data/provider/server/all.dart index 07efa67233..867c3af27d 100644 --- a/lib/data/provider/server/all.dart +++ b/lib/data/provider/server/all.dart @@ -41,7 +41,7 @@ class ServersNotifier extends _$ServersNotifier { } Future reload() async { - Stores.server.invalidateCache(); + Stores.server.dropCache(); final newState = _load(); if (newState == state) return; state = newState; diff --git a/lib/data/provider/server/all.g.dart b/lib/data/provider/server/all.g.dart index a463a84268..e7c4290156 100644 --- a/lib/data/provider/server/all.g.dart +++ b/lib/data/provider/server/all.g.dart @@ -41,7 +41,7 @@ final class ServersNotifierProvider } } -String _$serversNotifierHash() => r'103abff9174657b892a7bc032bad918477e04fd1'; +String _$serversNotifierHash() => r'08e6a1a0cba7055e2eb6e205b89d55d07434beda'; abstract class _$ServersNotifier extends $Notifier { ServersState build(); diff --git a/lib/data/provider/snippet.dart b/lib/data/provider/snippet.dart index 93a8cfd772..4fbabce54d 100644 --- a/lib/data/provider/snippet.dart +++ b/lib/data/provider/snippet.dart @@ -26,7 +26,7 @@ class SnippetNotifier extends _$SnippetNotifier { } void reload() { - Stores.snippet.invalidateCache(); + Stores.snippet.dropCache(); final newState = _load(); if (newState == state) return; state = newState; @@ -74,39 +74,38 @@ class SnippetNotifier extends _$SnippetNotifier { } Future del(Snippet snippet) async { - final newSnippets = state.snippets.where((s) => s != snippet).toList(); + final newSnippets = state.snippets + .where((s) => s.id != snippet.id) + .toList(); final newTags = _computeTags(newSnippets); Stores.snippet.delete(snippet); state = state.copyWith(snippets: newSnippets, tags: newTags); bakSync.sync(milliDelay: 1000); } + /// A rename is an update of columns, not a delete and an insert: the two + /// carry the same id, and deleting first would tombstone a record that is + /// still there and take its tags and auto-run targets with it. Future update(Snippet old, Snippet newOne) async { + if (old.id != newOne.id) { + // `EntityStore.update` refuses this; going straight to `put` would let a + // caller insert a second record under the name of the first. + throw ArgumentError('cannot change the id of a Snippet'); + } final newSnippets = state.snippets - .map((s) => s == old ? newOne : s) + .map((s) => s.id == old.id ? newOne : s) .toList(); final newTags = _computeTags(newSnippets); - Stores.snippet.update(old, newOne); + Stores.snippet.put(newOne); state = state.copyWith(snippets: newSnippets, tags: newTags); bakSync.sync(milliDelay: 1000); } + /// One `UPDATE` over the tag rows, rather than rewriting every snippet that + /// carries the tag. Future renameTag(String old, String newOne) async { - final updatedSnippets = []; - for (final s in state.snippets) { - if (s.tags?.contains(old) ?? false) { - final newTags = Set.from(s.tags!); - newTags.remove(old); - newTags.add(newOne); - final updatedSnippet = s.copyWith(tags: newTags.toList()); - updatedSnippets.add(updatedSnippet); - Stores.snippet.put(updatedSnippet); - } else { - updatedSnippets.add(s); - } - } - final newTags = _computeTags(updatedSnippets); - state = state.copyWith(snippets: updatedSnippets, tags: newTags); + Stores.snippet.renameTag(old, newOne); + state = _load(); bakSync.sync(milliDelay: 1000); } } diff --git a/lib/data/provider/snippet.g.dart b/lib/data/provider/snippet.g.dart index 20f5278775..64072f9b9f 100644 --- a/lib/data/provider/snippet.g.dart +++ b/lib/data/provider/snippet.g.dart @@ -41,7 +41,7 @@ final class SnippetNotifierProvider } } -String _$snippetNotifierHash() => r'46297b84ec6497e5c454be6ffe32330b37c6a465'; +String _$snippetNotifierHash() => r'a6dd4f1bf7d3df445cdaf696df5312c143d35e15'; abstract class _$SnippetNotifier extends $Notifier { SnippetState build(); diff --git a/lib/data/res/store.dart b/lib/data/res/store.dart index e8d50653d7..b01ef643b7 100644 --- a/lib/data/res/store.dart +++ b/lib/data/res/store.dart @@ -3,6 +3,7 @@ import 'package:get_it/get_it.dart'; import 'package:server_box/data/store/agent_conversation.dart'; import 'package:server_box/data/store/connection_stats.dart'; import 'package:server_box/data/store/container.dart'; +import 'package:server_box/data/store/entity_store.dart'; import 'package:server_box/data/store/history.dart'; import 'package:server_box/data/store/migrations/m003_hive_to_sqlite.dart'; import 'package:server_box/data/store/port_forward.dart'; @@ -10,6 +11,7 @@ import 'package:server_box/data/store/private_key.dart'; import 'package:server_box/data/store/server.dart'; import 'package:server_box/data/store/setting.dart'; import 'package:server_box/data/store/snippet.dart'; +import 'package:server_box/data/store/tables.dart'; final GetIt getIt = GetIt.instance; @@ -26,22 +28,20 @@ abstract final class Stores { getIt(); static PortForwardStore get portForward => getIt(); - /// The stores whose contents count as something the user changed. + /// The key-value stores whose contents count as something the user changed. /// - /// [lastModTime] is read off these, and sync uses that number to decide which - /// side wins — so what belongs here is what a user edits, not what the app - /// records. `connectionStats` used to be in this list and is not: connecting - /// to a server is not an edit, and every attempt was marking the device as - /// holding the newer copy of everything. - static List get _kvStores => [ - setting, - server, - container, - key, - snippet, - history, - portForward, - ]; + /// [lastModTime] is read off these and off [_entityStores], and sync uses that + /// number to decide which side wins — so what belongs here is what a user + /// edits, not what the app records. `connectionStats` used to be in this list + /// and is not: connecting to a server is not an edit, and every attempt was + /// marking the device as holding the newer copy of everything. + static List get _kvStores => [setting, history]; + + /// The same question asked of the stores that own tables. + /// + /// `container` is absent because its rows are children of `server`: changing + /// a container host stamps the server that owns it. + static List get _entityStores => [server, key, snippet, portForward]; static Future init() async { getIt.registerLazySingleton(() => SettingStore.instance); @@ -62,18 +62,30 @@ abstract final class Stores { () => PortForwardStore.instance, ); - // First and on its own. `connectionStats` and `agentConversation` create - // their tables, which means reaching the database synchronously — and a - // `Future.wait` invokes every element before awaiting any of them, so - // batching them with the stores that are still opening the file would have - // them reach a database that is still null. It did, on every cold launch. + // First and on its own: everything below reaches the database, and a + // `Future.wait` invokes every element before awaiting any of them — so + // batching them with the call that is still opening the file has them + // reach a database that is still null. It did, on every cold launch. await SqliteStore.openDatabase(); + // Then the entity schema, before anything can read or migrate it. Creating + // it means opening Drift over this connection, which is why it is awaited + // here rather than done inside a migration: a migration runs in one + // synchronous transaction and cannot await anything. + await createTables(SqliteDb.instance); + + // The entity stores are singletons holding a list cache, and this may not + // be the database they last read: the sandbox import closes one, restores + // the previous file and calls back in here. A cache from the old one would + // outlive it, and nothing else would ever drop it. + for (final store in _entityStores) { + store.dropCache(); + } + await Future.wait([ ..._kvStores.map((store) => store.init()), - // Their own tables rather than rows in `kv`, so they create those. + // Not a table to create — only the per-launch sweep of expired rows. connectionStats.init(), - agentConversation.init(), ]); // Before every fixup below. Each of them writes a flag meaning "this device @@ -94,20 +106,15 @@ abstract final class Stores { var lastModTime = 0; for (final store in _kvStores) { final last = store.lastUpdateTs; - if (last == null) { - continue; - } - var lastModTimeTs = 0; - for (final item in last.entries) { - final ts = item.value; - if (ts > lastModTimeTs) { - lastModTimeTs = ts; - } - } - if (lastModTimeTs > lastModTime) { - lastModTime = lastModTimeTs; + if (last == null) continue; + for (final ts in last.values) { + if (ts > lastModTime) lastModTime = ts; } } + for (final store in _entityStores) { + final ts = store.lastModTime; + if (ts > lastModTime) lastModTime = ts; + } return lastModTime; } } diff --git a/lib/data/store/agent_conversation.dart b/lib/data/store/agent_conversation.dart index f404ad2bc2..612ab4878d 100644 --- a/lib/data/store/agent_conversation.dart +++ b/lib/data/store/agent_conversation.dart @@ -21,14 +21,14 @@ import 'package:sqlite3/sqlite3.dart'; /// Left out of backup and sync on purpose: these may contain terminal output /// and reasoning. class AgentConversationStore { - AgentConversationStore._() : _suffix = ''; + AgentConversationStore._(); - /// A distinct table name, so a test on `SqliteDb.openInMemory()` cannot - /// collide with another test's rows. + /// A second instance over the same tables. + /// + /// The table names are fixed by the schema now, so isolation between tests + /// comes from `SqliteDb.openInMemory()` being fresh per test. @visibleForTesting - AgentConversationStore.forTest() : _suffix = '_test'; - - final String _suffix; + AgentConversationStore.forTest(); static final instance = AgentConversationStore._(); @@ -36,16 +36,10 @@ class AgentConversationStore { static const maxItemsPerConversation = 240; static const maxCharactersPerConversation = 512000; - /// Key prefixes of the Hive box these tables replaced. Only [importRow] - /// still needs them. - /// - /// TODO: delete with `HiveImport`. - static const _conversationPrefix = 'conversation::'; - static const _activePrefix = 'active::'; - Database get _db => SqliteDb.instance; - String get _conv => 'agent_conversation$_suffix'; - String get _active => 'agent_active$_suffix'; + + static const _conv = 'agent_conversation'; + static const _active = 'agent_active_conversation'; final _changes = StreamController.broadcast(); @@ -55,27 +49,6 @@ class AgentConversationStore { /// a write it did not make itself, and every write goes through this class. Stream watch() => _changes.stream; - Future init() async { - _db.execute( - 'CREATE TABLE IF NOT EXISTS $_conv (' - ' id TEXT NOT NULL PRIMARY KEY,' - ' server_id TEXT NOT NULL,' - ' updated_at INTEGER NOT NULL,' - ' data TEXT NOT NULL' - ') WITHOUT ROWID;', - ); - _db.execute( - 'CREATE INDEX IF NOT EXISTS idx_${_conv}_server_updated ' - 'ON $_conv(server_id, updated_at DESC);', - ); - _db.execute( - 'CREATE TABLE IF NOT EXISTS $_active (' - ' server_id TEXT NOT NULL PRIMARY KEY,' - ' conversation_id TEXT NOT NULL' - ') WITHOUT ROWID;', - ); - } - List fetchForServer(String serverId) { final rows = _db.select( 'SELECT data FROM $_conv WHERE server_id = ? ORDER BY updated_at DESC;', @@ -183,26 +156,29 @@ class AgentConversationStore { void deleteConversation(String serverId, String conversationId) { final conversation = fetch(conversationId); if (conversation == null || conversation.serverId != serverId) return; - _db.execute('DELETE FROM $_conv WHERE id = ?;', [conversationId]); + // Asked before the delete: the active row references the conversation and + // cascades with it, so afterwards there is nothing left to compare. + final wasActive = activeConversationId(serverId) == conversationId; + // One unit: the delete cascades the active row away, so committing it + // without the replacement would leave the server with none. + SqliteStore.transact(() { + _db.execute('DELETE FROM $_conv WHERE id = ?;', [conversationId]); + if (wasActive) { + final remaining = fetchForServer(serverId); + if (remaining.isNotEmpty) _setActiveRow(serverId, remaining.first.id); + } + }); // Exactly once, whichever way this returns. Deleting a conversation that // was not the active one used to return before notifying at all, leaving // the list showing a row that is gone; promoting a replacement notified // twice, because `setActive` notifies too. - if (activeConversationId(serverId) == conversationId) { - final remaining = fetchForServer(serverId); - if (remaining.isEmpty) { - _db.execute('DELETE FROM $_active WHERE server_id = ?;', [serverId]); - } else { - _setActiveRow(serverId, remaining.first.id); - } - } _changes.add(null); } + /// The active row goes with the conversations it references, by cascade. void clearServer(String serverId) { _db.execute('DELETE FROM $_conv WHERE server_id = ?;', [serverId]); - _db.execute('DELETE FROM $_active WHERE server_id = ?;', [serverId]); _changes.add(null); } @@ -308,25 +284,6 @@ class AgentConversationStore { return ''; } - /// Takes one row out of the Hive box these tables replaced. - /// - /// Used only by `HiveImport`. The box held both kinds under one namespace, - /// told apart by the key prefix. - bool importRow(String key, Object value) { - if (key.startsWith(_activePrefix)) { - if (value is! String || value.isEmpty) return false; - _setActiveRow(key.substring(_activePrefix.length), value); - return true; - } - if (!key.startsWith(_conversationPrefix) || value is! Map) return false; - final conversation = _fromMap(Map.from(value)); - if (conversation == null) return false; - // Not through `save`: that would re-trim and re-title a record the user - // already has, and prune against a table still being filled. - _upsert(conversation); - return true; - } - void _upsert(AgentConversation conversation) { _db.execute( 'INSERT INTO $_conv (id, server_id, updated_at, data) VALUES (?, ?, ?, ?) ' diff --git a/lib/data/store/cached_store.dart b/lib/data/store/cached_store.dart deleted file mode 100644 index ef02085fc8..0000000000 --- a/lib/data/store/cached_store.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:fl_lib/fl_lib.dart'; - -/// A [SqliteStore] whose rows are one model type, kept in a list cache. -/// -/// The cache is dropped by the write methods themselves rather than by watching -/// the store. Hive needed a watcher because a box could be written behind the -/// store's back — `Backup.restore` did exactly that — and a watcher then had to -/// be suppressed around the store's own writes so they did not each cost a -/// reload. Nothing can reach the database except through here now, so -/// invalidating in [set], [remove] and [clear] covers every path. -abstract class CachedSqliteStore extends SqliteStore { - CachedSqliteStore(super.name); - - List? _cache; - - List? get cachedItems => _cache; - - /// The row key for [item]. - String getKey(T item); - - /// Rebuilds one item from its stored JSON. - T? fromJson(Map json); - - @override - bool set( - String key, - V val, { - StoreToObj? toObj, - bool? updateLastUpdateTsOnSet, - }) { - final res = super.set( - key, - val, - toObj: toObj, - updateLastUpdateTsOnSet: updateLastUpdateTsOnSet, - ); - if (res) _cache = null; - return res; - } - - @override - bool remove(String key, {bool? updateLastUpdateTsOnRemove}) { - final res = super.remove( - key, - updateLastUpdateTsOnRemove: updateLastUpdateTsOnRemove, - ); - if (res) _cache = null; - return res; - } - - @override - Future clear({bool? updateLastUpdateTsOnClear}) async { - try { - return await super.clear( - updateLastUpdateTsOnClear: updateLastUpdateTsOnClear, - ); - } finally { - _cache = null; - } - } - - void invalidateCache() => _cache = null; - - void put(T item) => set(getKey(item), item); - - /// A copy, so a caller sorting or filtering the result cannot reorder the - /// cache everyone else reads. - List fetch() => List.from(_cache ??= _loadAll()); - - /// One query, not one per key. - /// - /// Under Hive `box.get` was a map lookup, so reading each key in turn was - /// free; each is a prepared-statement round trip now, and this runs on every - /// cache miss — which is every write, since the write methods drop the cache. - List _loadAll() { - final result = []; - for (final entry in getAllMap().entries) { - final raw = entry.value; - if (raw is! Map) continue; - try { - final item = fromJson(Map.from(raw)); - if (item != null) result.add(item); - } catch (e) { - dprint('Parsing $T from JSON', e); - } - } - return result; - } - - /// Reads one row, bypassing the cache. - T? fetchOneRaw(String key) { - final raw = get(key); - if (raw is! Map) return null; - try { - return fromJson(Map.from(raw)); - } catch (e) { - dprint('Parsing $T from JSON', e); - return null; - } - } - - void deleteById(String id) => remove(id); - - void delete(T item) => deleteById(getKey(item)); - - void update(T old, T newItem) { - final oldKey = getKey(old); - if (!have(old)) { - throw Exception('Old $T: $old not found'); - } - final newKey = getKey(newItem); - if (oldKey == newKey) { - // In place, which is what every caller but the id migrations does. An - // upsert is one statement and needs no transaction around it. - set(newKey, newItem); - return; - } - // The key moved, so this is a delete and an insert. As one unit: a crash - // between them would leave the record under neither key. `transact` nests, - // so this is also safe if a caller has already opened one. - SqliteStore.transact(() { - remove(oldKey); - set(newKey, newItem); - }); - } - - bool have(T item) => get(getKey(item)) != null; -} diff --git a/lib/data/store/connection_stats.dart b/lib/data/store/connection_stats.dart index d3eec8f55f..a97d3a3d1c 100644 --- a/lib/data/store/connection_stats.dart +++ b/lib/data/store/connection_stats.dart @@ -31,35 +31,24 @@ class ConnectionStatsStore { Database get _db => SqliteDb.instance; - Future init() async { - _db.execute(''' -CREATE TABLE IF NOT EXISTS conn_stat ( - id TEXT NOT NULL PRIMARY KEY, - server_id TEXT NOT NULL, - server_name TEXT NOT NULL, - timestamp INTEGER NOT NULL, - result TEXT NOT NULL, - error_message TEXT NOT NULL DEFAULT '', - duration_ms INTEGER NOT NULL -) WITHOUT ROWID; -'''); - // Every read is "this server, newest first", and the per-server cap below - // is the same order with a LIMIT. - _db.execute( - 'CREATE INDEX IF NOT EXISTS idx_conn_stat_server_ts ' - 'ON conn_stat(server_id, timestamp DESC);', - ); - // The age sweep asks about `timestamp` alone, which the index above cannot - // serve — its leading column is `server_id`. - _db.execute( - 'CREATE INDEX IF NOT EXISTS idx_conn_stat_ts ON conn_stat(timestamp);', - ); - - _expire(); - } + /// The table is created with the rest of the schema, so this is only the + /// per-launch age sweep. + Future init() async => _expire(); + /// Records one attempt, or does nothing if the server is gone. + /// + /// `server_id` is a foreign key now: a status refresh that lands after the + /// user deleted the server has nothing to attach the record to, and the row + /// would only ever have been orphaned statistics. Future recordConnection(ConnectionStat stat) async { - _insert(_idOf(stat), stat); + try { + _insert(ShortId.generate(), stat); + } on SqliteException catch (e) { + if (e.extendedResultCode == 787 /* SQLITE_CONSTRAINT_FOREIGNKEY */ ) { + return; + } + rethrow; + } _prune(stat.serverId); } @@ -236,30 +225,14 @@ CREATE TABLE IF NOT EXISTS conn_stat ( /// the compaction this feeds is `VACUUM` on that file. Future dbSizeAsync() => SqliteDb.size(); - /// Takes one row out of the Hive box this table replaced. - /// - /// Used only by `HiveImport`. It writes the record under the key it had, so - /// re-running the import overwrites rather than duplicates. - bool importRow(String key, Object value) { - if (value is! Map) return false; - try { - _insert(key, ConnectionStat.fromJson(Map.from(value))); - return true; - } catch (e) { - dprint('Importing ConnectionStat', e); - return false; - } - } - + /// The id is generated rather than `_`, which two attempts + /// in the same millisecond shared — the second overwrote the first, and the + /// counters are computed from these rows. void _insert(String id, ConnectionStat stat) { _db.execute( 'INSERT INTO conn_stat ' '(id, server_id, server_name, timestamp, result, error_message, duration_ms) ' - 'VALUES (?, ?, ?, ?, ?, ?, ?) ' - 'ON CONFLICT (id) DO UPDATE SET ' - 'server_name = excluded.server_name, timestamp = excluded.timestamp, ' - 'result = excluded.result, error_message = excluded.error_message, ' - 'duration_ms = excluded.duration_ms;', + 'VALUES (?, ?, ?, ?, ?, ?, ?);', [ id, stat.serverId, @@ -272,9 +245,6 @@ CREATE TABLE IF NOT EXISTS conn_stat ( ); } - static String _idOf(ConnectionStat stat) => - '${stat.serverId}_${stat.timestamp.millisecondsSinceEpoch}'; - static ConnectionStat _fromRow(Row row) => ConnectionStat( serverId: row['server_id'] as String, serverName: row['server_name'] as String, diff --git a/lib/data/store/container.dart b/lib/data/store/container.dart index b00f23214e..81aefb63f4 100644 --- a/lib/data/store/container.dart +++ b/lib/data/store/container.dart @@ -1,57 +1,176 @@ import 'package:fl_lib/fl_lib.dart'; +import 'package:meta/meta.dart'; import 'package:server_box/data/model/container/type.dart'; import 'package:server_box/data/res/store.dart'; +import 'package:sqlite3/sqlite3.dart'; -const _keyConfig = 'providerConfig'; -const _keyHost = 'containerHost'; +/// Per-server container settings: the runtime the user picked, and the host to +/// reach it on. +/// +/// Not an [EntityStore]: both are children of `server`, so neither carries its +/// own `updated_at` and there is nothing here to tombstone. A change is a +/// change to the server, which is what [ServerStore.touch] records — and what +/// makes the row travel with the server it belongs to instead of arriving +/// before it. +/// +/// The keys these replaced were `containerHost` and +/// `providerConfig` in one K-V store, so nothing stopped a host +/// belonging to a server that had been deleted, and a serverId beginning with +/// `docker` would have been read as a runtime name. +class ContainerStore { + ContainerStore._(); -class ContainerStore extends SqliteStore { - ContainerStore._() : super('docker'); + /// See [PrivateKeyStore.forTest]. + @visibleForTesting + ContainerStore.forTest(); static final instance = ContainerStore._(); - String? fetch(String? id, ContainerType type) { - final host = get(_hostKey(id, type)); - if (host != null || type == ContainerType.podman) return host; + Database get _db => SqliteDb.instance; - // Preserve existing Docker host settings stored before per-runtime hosts. - return id == null ? null : get(id); + /// The host for [id] under [type], or null to use the runtime's own default. + String? fetch(String? id, ContainerType type) { + if (id == null) return null; + final rows = _db.select( + 'SELECT host FROM container_host WHERE server_id = ? AND type = ?;', + [id, type.name], + ); + return rows.singleOrNull?['host'] as String?; } void put(String id, ContainerType type, String host) { - set(_hostKey(id, type), host); + SqliteStore.transact(() { + _db.execute( + 'INSERT INTO container_host (server_id, type, host) VALUES (?, ?, ?) ' + 'ON CONFLICT (server_id, type) DO UPDATE SET host = excluded.host;', + [id, type.name, host], + ); + Stores.server.synced.stamp(id); + }); + Stores.server.invalidate(); } void removeHost(String id, ContainerType type) { - remove(_hostKey(id, type)); - remove(id); + SqliteStore.transact(() { + _db.execute( + 'DELETE FROM container_host WHERE server_id = ? AND type = ?;', + [id, type.name], + ); + Stores.server.synced.stamp(id); + }); + Stores.server.invalidate(); } - String _hostKey(String? id, ContainerType type) => - '$_keyHost${type.name}${id ?? ''}'; + /// The runtime chosen for [id], falling back to the global default. + ContainerType getType(String id) { + final rows = _db.select( + 'SELECT type FROM container_runtime WHERE server_id = ?;', + [id], + ); + final name = rows.singleOrNull?['type'] as String?; + return ContainerType.values.firstWhereOrNull((e) => e.name == name) ?? + defaultType; + } - ContainerType getType([String id = '']) { - final cfg = get(_keyConfig + id); - if (cfg != null) { - final type = ContainerType.values.firstWhereOrNull( - (e) => e.toString() == cfg, - ); - if (type != null) return type; + ContainerType get defaultType => + Stores.setting.usePodman.get() ? ContainerType.podman : ContainerType.docker; + + /// Records [type] for [id], or drops the row when it matches the default — + /// so a later change to the global setting still reaches servers the user + /// never chose one for. + void setType(ContainerType type, String id) { + SqliteStore.transact(() { + if (type == defaultType) { + _db.execute('DELETE FROM container_runtime WHERE server_id = ?;', [id]); + } else { + _db.execute( + 'INSERT INTO container_runtime (server_id, type) VALUES (?, ?) ' + 'ON CONFLICT (server_id) DO UPDATE SET type = excluded.type;', + [id, type.name], + ); + } + Stores.server.synced.stamp(id); + }); + Stores.server.invalidate(); + } + + /// Everything this store holds, keyed the way a backup carries it. + /// + /// One entry per server, so a restore writes both tables from one object + /// rather than reconstructing the old flattened key names. + Map getAllMap() { + final out = >{}; + for (final row in _db.select( + 'SELECT server_id, type, host FROM container_host;', + )) { + (out[row['server_id'] as String] ??= {})['host_${row['type']}'] = + row['host'] as String; + } + for (final row in _db.select( + 'SELECT server_id, type FROM container_runtime;', + )) { + (out[row['server_id'] as String] ??= {})['runtime'] = + row['type'] as String; } + return out; + } + + /// Restores the flattened map the v1 backup format carries. + /// + /// That format holds this store's old K-V keys verbatim: + /// `containerHost`, `providerConfig`, and a bare + /// `` for the Docker host from before there was a runtime to name. + void restoreLegacyMap(Map flat) { + const hostPrefix = 'containerHost'; + const typePrefix = 'providerConfig'; - return defaultType; + flat.forEach((key, value) { + if (value is! String || value.isEmpty) return; + if (key.startsWith(hostPrefix)) { + final rest = key.substring(hostPrefix.length); + final type = ContainerType.values.firstWhereOrNull( + (e) => rest.startsWith(e.name), + ); + if (type == null) return; + _restoreHost(rest.substring(type.name.length), type, value); + return; + } + if (key.startsWith(typePrefix)) { + final serverId = key.substring(typePrefix.length); + final type = ContainerType.values.firstWhereOrNull( + (e) => value.endsWith(e.name), + ); + if (type == null || !_known(serverId)) return; + setType(type, serverId); + return; + } + _restoreHost(key, ContainerType.docker, value); + }); } - ContainerType get defaultType { - if (Stores.setting.usePodman.get()) return ContainerType.podman; - return ContainerType.docker; + void _restoreHost(String serverId, ContainerType type, String host) { + if (!_known(serverId)) return; + put(serverId, type, host); } - void setType(ContainerType type, [String id = '']) { - if (type == defaultType) { - remove(_keyConfig + id); - } else { - set(_keyConfig + id, type.toString()); + /// Both tables have a foreign key, and a backup can name a server this device + /// deleted. + bool _known(String serverId) => + Stores.server.fetchOneRaw(serverId) != null; + + /// Writes one entry of [getAllMap] back. Skips a server that is not here: + /// both tables have a foreign key, and a backup can name a server this + /// device deleted. + void restoreOne(String serverId, Object? value) { + if (value is! Map) return; + if (!_known(serverId)) return; + for (final type in ContainerType.values) { + final host = value['host_${type.name}']; + if (host is String && host.isNotEmpty) put(serverId, type, host); } + final runtime = ContainerType.values.firstWhereOrNull( + (e) => e.name == value['runtime'], + ); + if (runtime != null) setType(runtime, serverId); } } diff --git a/lib/data/store/db.dart b/lib/data/store/db.dart new file mode 100644 index 0000000000..72f54d4618 --- /dev/null +++ b/lib/data/store/db.dart @@ -0,0 +1,518 @@ +import 'package:drift/drift.dart'; + +part 'db.g.dart'; + +/// The entity schema, as Drift sees it. +/// +/// Drift owns the DDL: two sources for one schema is the failure this whole +/// change exists to stop, and `tables_schema_test.dart` is the acceptance +/// test — every guarantee it asserts has to hold against what Drift creates. +/// +/// Data classes carry a `Row` suffix because the app's own models are the +/// external interface and several share these names: a Drift `Snippet` and +/// the freezed `Snippet` in the same file would be unreadable. +/// +/// What is *not* here: `setting` and `history`. Those stay rows in `kv` — 103 +/// unrelated preferences with no relations and nothing that queries by field, +/// where adding one should stay a one-line change rather than a migration. + +/// Columns every syncable record carries. +/// +/// `updatedAt` is what an incremental pull selects on; `rev` separates two +/// edits inside one millisecond, which a clock cannot. +mixin SyncMeta on Table { + IntColumn get updatedAt => integer().withDefault(const Constant(0))(); + IntColumn get rev => integer().withDefault(const Constant(0))(); +} + +/// `name` is what the user typed and what the UI lists; `id` is what +/// [Servers.sshKeyId] points at, so renaming a key cannot orphan a server. +@DataClassName('PrivateKeyRow') +class PrivateKeys extends Table with SyncMeta { + @override + String get tableName => 'private_key'; + @override + bool get withoutRowId => true; + + TextColumn get id => text()(); + TextColumn get name => text().unique()(); + TextColumn get key => text()(); + + @override + Set get primaryKey => {id}; +} + +/// The SSH and monitor credentials are columns rather than tables of their +/// own: both are one-to-one and neither is ever read without the server. +/// +/// The final constraint is `ServerConnectCredential.fromSpi`'s rule written +/// where it cannot be forgotten. The app enforced it in `Spix.validate()` +/// alone, so a record could be stored with both or neither and only fail +/// later, at connect time. +@DataClassName('ServerRow') +class Servers extends Table with SyncMeta { + @override + String get tableName => 'server'; + @override + bool get withoutRowId => true; + + TextColumn get id => text()(); + TextColumn get name => text()(); + BoolColumn get autoConnect => boolean().withDefault(const Constant(true))(); + TextColumn get systemType => text().nullable()(); + + TextColumn get sshIp => text().nullable()(); + // The range check is a table constraint: a column cannot reference itself + // in its own `check()`. + IntColumn get sshPort => integer().nullable()(); + TextColumn get sshUser => text().nullable()(); + TextColumn get sshPwd => text().nullable()(); + + /// Deleting a key must not delete the servers that used it; it must leave + /// them asking for a new one. + TextColumn get sshKeyId => text() + .nullable() + .references(PrivateKeys, #id, onDelete: KeyAction.setNull)(); + TextColumn get sshKeyPath => text().nullable()(); + TextColumn get sshAlterUrl => text().nullable()(); + TextColumn get sshProxyCommand => text().nullable()(); + + TextColumn get monitorAddr => text().nullable()(); + TextColumn get monitorUser => text().nullable()(); + TextColumn get monitorPwd => text().nullable()(); + BoolColumn get monitorIgnoreCert => boolean().nullable()(); + + TextColumn get wolMac => text().nullable()(); + TextColumn get wolIp => text().nullable()(); + TextColumn get wolPwd => text().nullable()(); + + TextColumn get pveAddr => text().nullable()(); + BoolColumn get pveIgnoreCert => boolean().withDefault(const Constant(false))(); + TextColumn get pvePwd => text().nullable()(); + TextColumn get preferTempDev => text().nullable()(); + BoolColumn get tempIsCelsius => boolean().withDefault(const Constant(true))(); + TextColumn get logoUrl => text().nullable()(); + TextColumn get netDev => text().nullable()(); + TextColumn get scriptDir => text().nullable()(); + + @override + Set get primaryKey => {id}; + + @override + List get customConstraints => [ + // Reached over SSH or over a monitor agent, never both and never neither. + 'CHECK ((ssh_ip IS NOT NULL) <> (monitor_addr IS NOT NULL))', + 'CHECK (ssh_port IS NULL OR ssh_port BETWEEN 1 AND 65535)', + ]; +} + +/// A tag was an element of a JSON array, so "which servers have this tag" +/// meant decoding every server. It is an index lookup now. +@DataClassName('ServerTagRow') +class ServerTags extends Table { + @override + String get tableName => 'server_tag'; + @override + bool get withoutRowId => true; + + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + TextColumn get tag => text()(); + + @override + Set get primaryKey => {serverId, tag}; +} + +@DataClassName('ServerEnvRow') +class ServerEnvs extends Table { + @override + String get tableName => 'server_env'; + @override + bool get withoutRowId => true; + + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + TextColumn get key => text()(); + TextColumn get value => text()(); + + @override + Set get primaryKey => {serverId, key}; +} + +/// `ord` keeps the order the user chose: at most the first two are used as +/// failover candidates, so which comes first is meaningful. +@DataClassName('ServerJumpRow') +class ServerJumps extends Table { + @override + String get tableName => 'server_jump'; + @override + bool get withoutRowId => true; + + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + IntColumn get ord => integer()(); + TextColumn get jumpId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {serverId, ord}; +} + +@DataClassName('ServerDisabledCmdRow') +class ServerDisabledCmds extends Table { + @override + String get tableName => 'server_disabled_cmd'; + @override + bool get withoutRowId => true; + + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + TextColumn get cmdType => text()(); + + @override + Set get primaryKey => {serverId, cmdType}; +} + +/// `ServerCustom.cmds`, a map of name -> command. +@DataClassName('ServerCustomCmdRow') +class ServerCustomCmds extends Table { + @override + String get tableName => 'server_custom_cmd'; + @override + bool get withoutRowId => true; + + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + TextColumn get name => text()(); + TextColumn get cmd => text()(); + + @override + Set get primaryKey => {serverId, name}; +} + +/// Was a JSON map in `setting` keyed `::`, so deleting a +/// server left its trusted fingerprints behind for ever with nothing to say +/// whose they were. +@DataClassName('KnownHostRow') +class KnownHosts extends Table { + @override + String get tableName => 'known_host'; + @override + bool get withoutRowId => true; + + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + TextColumn get keyType => text()(); + TextColumn get fingerprint => text()(); + + @override + Set get primaryKey => {serverId, keyType}; +} + +@DataClassName('SnippetRow') +class Snippets extends Table with SyncMeta { + @override + String get tableName => 'snippet'; + @override + bool get withoutRowId => true; + + TextColumn get id => text()(); + TextColumn get name => text().unique()(); + TextColumn get script => text()(); + TextColumn get note => text().nullable()(); + + @override + Set get primaryKey => {id}; +} + +@DataClassName('SnippetTagRow') +class SnippetTags extends Table { + @override + String get tableName => 'snippet_tag'; + @override + bool get withoutRowId => true; + + TextColumn get snippetId => + text().references(Snippets, #id, onDelete: KeyAction.cascade)(); + TextColumn get tag => text()(); + + @override + Set get primaryKey => {snippetId, tag}; +} + +/// Both sides cascade: a deleted snippet stops being auto-run, and a deleted +/// server stops being a target. Neither used to happen. +@DataClassName('SnippetAutoRunRow') +class SnippetAutoRunOn extends Table { + @override + String get tableName => 'snippet_auto_run_on'; + @override + bool get withoutRowId => true; + + TextColumn get snippetId => + text().references(Snippets, #id, onDelete: KeyAction.cascade)(); + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {snippetId, serverId}; +} + +@DataClassName('PortForwardRow') +class PortForwards extends Table with SyncMeta { + @override + String get tableName => 'port_forward'; + @override + bool get withoutRowId => true; + + TextColumn get id => text()(); + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + TextColumn get name => text()(); + TextColumn get type => text()(); + TextColumn get localHost => text().nullable()(); + IntColumn get localPort => integer().withDefault(const Constant(0))(); + TextColumn get remoteHost => text().nullable()(); + IntColumn get remotePort => integer().nullable()(); + + @override + Set get primaryKey => {id}; + + @override + List get customConstraints => [ + "CHECK (type IN ('local', 'remote', 'dynamic'))", + ]; +} + +/// `DOCKER_HOST` (or the podman equivalent) for one server. +/// +/// A child of `server` rather than a record of its own: it is per-server +/// configuration with no meaning apart from the server, so it cascades with it +/// and the parent's `updated_at` is what moves when it changes — the same rule +/// tags and envs follow. +@DataClassName('ContainerHostRow') +class ContainerHosts extends Table { + @override + String get tableName => 'container_host'; + @override + bool get withoutRowId => true; + + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + TextColumn get type => text()(); + TextColumn get host => text()(); + + @override + Set get primaryKey => {serverId, type}; + + @override + List get customConstraints => [ + "CHECK (type IN ('docker', 'podman'))", + ]; +} + +/// Which runtime the user picked for one server, where absent means the global +/// default (`SettingStore.usePodman`). +/// +/// Was `providerConfig` in the `docker` K-V store, holding +/// `ContainerType.podman` — the result of `toString()` on an enum, so a +/// renamed case would have read as the default without saying so. +@DataClassName('ContainerRuntimeRow') +class ContainerRuntimes extends Table { + @override + String get tableName => 'container_runtime'; + @override + bool get withoutRowId => true; + + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + TextColumn get type => text()(); + + @override + Set get primaryKey => {serverId}; + + @override + List get customConstraints => [ + "CHECK (type IN ('docker', 'podman'))", + ]; +} + +/// `id` is generated rather than `_`: two attempts in the +/// same millisecond shared that key and the second overwrote the first, which +/// the counters are computed from. +/// +/// `serverName` stays denormalised on purpose — it is what the server was +/// called at the time, and renaming it must not rewrite its history. +@DataClassName('ConnStatRow') +class ConnStats extends Table { + @override + String get tableName => 'conn_stat'; + @override + bool get withoutRowId => true; + + TextColumn get id => text()(); + TextColumn get serverId => + text().references(Servers, #id, onDelete: KeyAction.cascade)(); + TextColumn get serverName => text()(); + IntColumn get timestamp => integer()(); + TextColumn get result => text()(); + TextColumn get errorMessage => text().withDefault(const Constant(''))(); + IntColumn get durationMs => integer()(); + + @override + Set get primaryKey => {id}; +} + +/// `data` stays JSON: a conversation is an ordered log of heterogeneous items +/// read only ever whole, never queried by field. Columns would buy nothing and +/// cost a migration for every new item kind. +/// +/// `serverId` has no foreign key: the global agent uses a scope id that is not +/// a server. +/// +/// No sync columns, and not a sync root: a conversation carries terminal output +/// and reasoning, so it is deliberately left out of backup and sync. Its +/// `updatedAt` is the conversation's own timestamp — what the list is ordered +/// by and what the per-server cap keeps — rather than a record of when this +/// device last touched the row. +@DataClassName('AgentConversationRow') +class AgentConversations extends Table { + @override + String get tableName => 'agent_conversation'; + @override + bool get withoutRowId => true; + + TextColumn get id => text()(); + TextColumn get serverId => text()(); + IntColumn get updatedAt => integer()(); + TextColumn get data => text()(); + + @override + Set get primaryKey => {id}; +} + +@DataClassName('AgentActiveConversationRow') +class AgentActiveConversations extends Table { + @override + String get tableName => 'agent_active_conversation'; + @override + bool get withoutRowId => true; + + TextColumn get serverId => text()(); + TextColumn get conversationId => text() + .references(AgentConversations, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {serverId}; +} + +/// A delete has to be a fact that can travel. Without this the peer that still +/// holds the row reads its absence as an addition and puts it back, which is +/// how a deleted server returns on the next sync. +@DataClassName('TombstoneRow') +class Tombstones extends Table { + @override + String get tableName => 'tombstone'; + @override + bool get withoutRowId => true; + + TextColumn get tbl => text()(); + TextColumn get rowId => text()(); + IntColumn get deletedAt => integer()(); + + @override + Set get primaryKey => {tbl, rowId}; +} + +/// This device's id and how far it has read each peer's log. Key-value because +/// it is bookkeeping *about* sync rather than anything synced, and it must +/// never itself be uploaded. +@DataClassName('SyncStateRow') +class SyncStates extends Table { + @override + String get tableName => 'sync_state'; + @override + bool get withoutRowId => true; + + TextColumn get k => text()(); + TextColumn get v => text()(); + + @override + Set get primaryKey => {k}; +} + +/// The database, over the connection the app already opened and keyed. +/// +/// Drift is a query and mapping layer here, not the owner of the connection: +/// `SqliteDb` opens the file, applies `PRAGMA cipher`/`PRAGMA key` and +/// `foreign_keys`, and hands the live handle over. Letting Drift open its own +/// would mean an unencrypted database and a connection without the pragmas — +/// `foreign_keys` is per-connection. +/// +/// [schemaVersion] is 1 and stays there. Version is tracked by +/// `SchemaVersion`, because the steps that matter are outside what a Drift +/// migration can say: m003 reads Hive boxes, m004 remaps ids and rewrites the +/// references between them. Two mechanisms each advancing the same number +/// would be the ambiguity this file exists to remove. +@DriftDatabase( + tables: [ + PrivateKeys, + Servers, + ServerTags, + ServerEnvs, + ServerJumps, + ServerDisabledCmds, + ServerCustomCmds, + KnownHosts, + Snippets, + SnippetTags, + SnippetAutoRunOn, + PortForwards, + ContainerHosts, + ContainerRuntimes, + ConnStats, + AgentConversations, + AgentActiveConversations, + Tombstones, + SyncStates, + ], +) +class AppDb extends _$AppDb { + AppDb(super.e); + + @override + int get schemaVersion => 1; + + @override + MigrationStrategy get migration => MigrationStrategy( + onCreate: (m) async { + await m.createAll(); + for (final statement in _indexes) { + await customStatement(statement); + } + }, + ); + + /// Indexes the read patterns need, which the table definitions do not carry. + static const _indexes = [ + 'CREATE INDEX IF NOT EXISTS idx_server_key ON server(ssh_key_id);', + 'CREATE INDEX IF NOT EXISTS idx_server_tag_tag ON server_tag(tag);', + 'CREATE INDEX IF NOT EXISTS idx_server_jump_target ON server_jump(jump_id);', + 'CREATE INDEX IF NOT EXISTS idx_snippet_tag_tag ON snippet_tag(tag);', + 'CREATE INDEX IF NOT EXISTS idx_snippet_auto_run_server ' + 'ON snippet_auto_run_on(server_id);', + 'CREATE INDEX IF NOT EXISTS idx_port_forward_server ' + 'ON port_forward(server_id);', + // Every read is "this server, newest first"; the per-server cap is the + // same order with a LIMIT. + 'CREATE INDEX IF NOT EXISTS idx_conn_stat_server_ts ' + 'ON conn_stat(server_id, timestamp DESC);', + // The age sweep asks about `timestamp` alone, which the index above + // cannot serve — its leading column is `server_id`. + 'CREATE INDEX IF NOT EXISTS idx_conn_stat_ts ON conn_stat(timestamp);', + 'CREATE INDEX IF NOT EXISTS idx_agent_conversation_server_updated ' + 'ON agent_conversation(server_id, updated_at DESC);', + 'CREATE INDEX IF NOT EXISTS idx_tombstone_deleted ' + 'ON tombstone(deleted_at);', + ]; +} diff --git a/lib/data/store/db.g.dart b/lib/data/store/db.g.dart new file mode 100644 index 0000000000..8e28b04373 --- /dev/null +++ b/lib/data/store/db.g.dart @@ -0,0 +1,13816 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'db.dart'; + +// ignore_for_file: type=lint +class $PrivateKeysTable extends PrivateKeys + with TableInfo<$PrivateKeysTable, PrivateKeyRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PrivateKeysTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _revMeta = const VerificationMeta('rev'); + @override + late final GeneratedColumn rev = GeneratedColumn( + 'rev', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _keyMeta = const VerificationMeta('key'); + @override + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [updatedAt, rev, id, name, key]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'private_key'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('rev')) { + context.handle( + _revMeta, + rev.isAcceptableOrUnknown(data['rev']!, _revMeta), + ); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('key')) { + context.handle( + _keyMeta, + key.isAcceptableOrUnknown(data['key']!, _keyMeta), + ); + } else if (isInserting) { + context.missing(_keyMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PrivateKeyRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PrivateKeyRow( + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + rev: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rev'], + )!, + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + ); + } + + @override + $PrivateKeysTable createAlias(String alias) { + return $PrivateKeysTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class PrivateKeyRow extends DataClass implements Insertable { + final int updatedAt; + final int rev; + final String id; + final String name; + final String key; + const PrivateKeyRow({ + required this.updatedAt, + required this.rev, + required this.id, + required this.name, + required this.key, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['updated_at'] = Variable(updatedAt); + map['rev'] = Variable(rev); + map['id'] = Variable(id); + map['name'] = Variable(name); + map['key'] = Variable(key); + return map; + } + + PrivateKeysCompanion toCompanion(bool nullToAbsent) { + return PrivateKeysCompanion( + updatedAt: Value(updatedAt), + rev: Value(rev), + id: Value(id), + name: Value(name), + key: Value(key), + ); + } + + factory PrivateKeyRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PrivateKeyRow( + updatedAt: serializer.fromJson(json['updatedAt']), + rev: serializer.fromJson(json['rev']), + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + key: serializer.fromJson(json['key']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'updatedAt': serializer.toJson(updatedAt), + 'rev': serializer.toJson(rev), + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'key': serializer.toJson(key), + }; + } + + PrivateKeyRow copyWith({ + int? updatedAt, + int? rev, + String? id, + String? name, + String? key, + }) => PrivateKeyRow( + updatedAt: updatedAt ?? this.updatedAt, + rev: rev ?? this.rev, + id: id ?? this.id, + name: name ?? this.name, + key: key ?? this.key, + ); + PrivateKeyRow copyWithCompanion(PrivateKeysCompanion data) { + return PrivateKeyRow( + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + rev: data.rev.present ? data.rev.value : this.rev, + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + key: data.key.present ? data.key.value : this.key, + ); + } + + @override + String toString() { + return (StringBuffer('PrivateKeyRow(') + ..write('updatedAt: $updatedAt, ') + ..write('rev: $rev, ') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('key: $key') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(updatedAt, rev, id, name, key); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PrivateKeyRow && + other.updatedAt == this.updatedAt && + other.rev == this.rev && + other.id == this.id && + other.name == this.name && + other.key == this.key); +} + +class PrivateKeysCompanion extends UpdateCompanion { + final Value updatedAt; + final Value rev; + final Value id; + final Value name; + final Value key; + const PrivateKeysCompanion({ + this.updatedAt = const Value.absent(), + this.rev = const Value.absent(), + this.id = const Value.absent(), + this.name = const Value.absent(), + this.key = const Value.absent(), + }); + PrivateKeysCompanion.insert({ + this.updatedAt = const Value.absent(), + this.rev = const Value.absent(), + required String id, + required String name, + required String key, + }) : id = Value(id), + name = Value(name), + key = Value(key); + static Insertable custom({ + Expression? updatedAt, + Expression? rev, + Expression? id, + Expression? name, + Expression? key, + }) { + return RawValuesInsertable({ + if (updatedAt != null) 'updated_at': updatedAt, + if (rev != null) 'rev': rev, + if (id != null) 'id': id, + if (name != null) 'name': name, + if (key != null) 'key': key, + }); + } + + PrivateKeysCompanion copyWith({ + Value? updatedAt, + Value? rev, + Value? id, + Value? name, + Value? key, + }) { + return PrivateKeysCompanion( + updatedAt: updatedAt ?? this.updatedAt, + rev: rev ?? this.rev, + id: id ?? this.id, + name: name ?? this.name, + key: key ?? this.key, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PrivateKeysCompanion(') + ..write('updatedAt: $updatedAt, ') + ..write('rev: $rev, ') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('key: $key') + ..write(')')) + .toString(); + } +} + +class $ServersTable extends Servers with TableInfo<$ServersTable, ServerRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ServersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _revMeta = const VerificationMeta('rev'); + @override + late final GeneratedColumn rev = GeneratedColumn( + 'rev', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _autoConnectMeta = const VerificationMeta( + 'autoConnect', + ); + @override + late final GeneratedColumn autoConnect = GeneratedColumn( + 'auto_connect', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("auto_connect" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); + static const VerificationMeta _systemTypeMeta = const VerificationMeta( + 'systemType', + ); + @override + late final GeneratedColumn systemType = GeneratedColumn( + 'system_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sshIpMeta = const VerificationMeta('sshIp'); + @override + late final GeneratedColumn sshIp = GeneratedColumn( + 'ssh_ip', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sshPortMeta = const VerificationMeta( + 'sshPort', + ); + @override + late final GeneratedColumn sshPort = GeneratedColumn( + 'ssh_port', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _sshUserMeta = const VerificationMeta( + 'sshUser', + ); + @override + late final GeneratedColumn sshUser = GeneratedColumn( + 'ssh_user', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sshPwdMeta = const VerificationMeta('sshPwd'); + @override + late final GeneratedColumn sshPwd = GeneratedColumn( + 'ssh_pwd', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sshKeyIdMeta = const VerificationMeta( + 'sshKeyId', + ); + @override + late final GeneratedColumn sshKeyId = GeneratedColumn( + 'ssh_key_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES private_key (id) ON DELETE SET NULL', + ), + ); + static const VerificationMeta _sshKeyPathMeta = const VerificationMeta( + 'sshKeyPath', + ); + @override + late final GeneratedColumn sshKeyPath = GeneratedColumn( + 'ssh_key_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sshAlterUrlMeta = const VerificationMeta( + 'sshAlterUrl', + ); + @override + late final GeneratedColumn sshAlterUrl = GeneratedColumn( + 'ssh_alter_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sshProxyCommandMeta = const VerificationMeta( + 'sshProxyCommand', + ); + @override + late final GeneratedColumn sshProxyCommand = GeneratedColumn( + 'ssh_proxy_command', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _monitorAddrMeta = const VerificationMeta( + 'monitorAddr', + ); + @override + late final GeneratedColumn monitorAddr = GeneratedColumn( + 'monitor_addr', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _monitorUserMeta = const VerificationMeta( + 'monitorUser', + ); + @override + late final GeneratedColumn monitorUser = GeneratedColumn( + 'monitor_user', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _monitorPwdMeta = const VerificationMeta( + 'monitorPwd', + ); + @override + late final GeneratedColumn monitorPwd = GeneratedColumn( + 'monitor_pwd', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _monitorIgnoreCertMeta = const VerificationMeta( + 'monitorIgnoreCert', + ); + @override + late final GeneratedColumn monitorIgnoreCert = GeneratedColumn( + 'monitor_ignore_cert', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("monitor_ignore_cert" IN (0, 1))', + ), + ); + static const VerificationMeta _wolMacMeta = const VerificationMeta('wolMac'); + @override + late final GeneratedColumn wolMac = GeneratedColumn( + 'wol_mac', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _wolIpMeta = const VerificationMeta('wolIp'); + @override + late final GeneratedColumn wolIp = GeneratedColumn( + 'wol_ip', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _wolPwdMeta = const VerificationMeta('wolPwd'); + @override + late final GeneratedColumn wolPwd = GeneratedColumn( + 'wol_pwd', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _pveAddrMeta = const VerificationMeta( + 'pveAddr', + ); + @override + late final GeneratedColumn pveAddr = GeneratedColumn( + 'pve_addr', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _pveIgnoreCertMeta = const VerificationMeta( + 'pveIgnoreCert', + ); + @override + late final GeneratedColumn pveIgnoreCert = GeneratedColumn( + 'pve_ignore_cert', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("pve_ignore_cert" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _pvePwdMeta = const VerificationMeta('pvePwd'); + @override + late final GeneratedColumn pvePwd = GeneratedColumn( + 'pve_pwd', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _preferTempDevMeta = const VerificationMeta( + 'preferTempDev', + ); + @override + late final GeneratedColumn preferTempDev = GeneratedColumn( + 'prefer_temp_dev', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _tempIsCelsiusMeta = const VerificationMeta( + 'tempIsCelsius', + ); + @override + late final GeneratedColumn tempIsCelsius = GeneratedColumn( + 'temp_is_celsius', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("temp_is_celsius" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); + static const VerificationMeta _logoUrlMeta = const VerificationMeta( + 'logoUrl', + ); + @override + late final GeneratedColumn logoUrl = GeneratedColumn( + 'logo_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _netDevMeta = const VerificationMeta('netDev'); + @override + late final GeneratedColumn netDev = GeneratedColumn( + 'net_dev', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _scriptDirMeta = const VerificationMeta( + 'scriptDir', + ); + @override + late final GeneratedColumn scriptDir = GeneratedColumn( + 'script_dir', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + updatedAt, + rev, + id, + name, + autoConnect, + systemType, + sshIp, + sshPort, + sshUser, + sshPwd, + sshKeyId, + sshKeyPath, + sshAlterUrl, + sshProxyCommand, + monitorAddr, + monitorUser, + monitorPwd, + monitorIgnoreCert, + wolMac, + wolIp, + wolPwd, + pveAddr, + pveIgnoreCert, + pvePwd, + preferTempDev, + tempIsCelsius, + logoUrl, + netDev, + scriptDir, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'server'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('rev')) { + context.handle( + _revMeta, + rev.isAcceptableOrUnknown(data['rev']!, _revMeta), + ); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('auto_connect')) { + context.handle( + _autoConnectMeta, + autoConnect.isAcceptableOrUnknown( + data['auto_connect']!, + _autoConnectMeta, + ), + ); + } + if (data.containsKey('system_type')) { + context.handle( + _systemTypeMeta, + systemType.isAcceptableOrUnknown(data['system_type']!, _systemTypeMeta), + ); + } + if (data.containsKey('ssh_ip')) { + context.handle( + _sshIpMeta, + sshIp.isAcceptableOrUnknown(data['ssh_ip']!, _sshIpMeta), + ); + } + if (data.containsKey('ssh_port')) { + context.handle( + _sshPortMeta, + sshPort.isAcceptableOrUnknown(data['ssh_port']!, _sshPortMeta), + ); + } + if (data.containsKey('ssh_user')) { + context.handle( + _sshUserMeta, + sshUser.isAcceptableOrUnknown(data['ssh_user']!, _sshUserMeta), + ); + } + if (data.containsKey('ssh_pwd')) { + context.handle( + _sshPwdMeta, + sshPwd.isAcceptableOrUnknown(data['ssh_pwd']!, _sshPwdMeta), + ); + } + if (data.containsKey('ssh_key_id')) { + context.handle( + _sshKeyIdMeta, + sshKeyId.isAcceptableOrUnknown(data['ssh_key_id']!, _sshKeyIdMeta), + ); + } + if (data.containsKey('ssh_key_path')) { + context.handle( + _sshKeyPathMeta, + sshKeyPath.isAcceptableOrUnknown( + data['ssh_key_path']!, + _sshKeyPathMeta, + ), + ); + } + if (data.containsKey('ssh_alter_url')) { + context.handle( + _sshAlterUrlMeta, + sshAlterUrl.isAcceptableOrUnknown( + data['ssh_alter_url']!, + _sshAlterUrlMeta, + ), + ); + } + if (data.containsKey('ssh_proxy_command')) { + context.handle( + _sshProxyCommandMeta, + sshProxyCommand.isAcceptableOrUnknown( + data['ssh_proxy_command']!, + _sshProxyCommandMeta, + ), + ); + } + if (data.containsKey('monitor_addr')) { + context.handle( + _monitorAddrMeta, + monitorAddr.isAcceptableOrUnknown( + data['monitor_addr']!, + _monitorAddrMeta, + ), + ); + } + if (data.containsKey('monitor_user')) { + context.handle( + _monitorUserMeta, + monitorUser.isAcceptableOrUnknown( + data['monitor_user']!, + _monitorUserMeta, + ), + ); + } + if (data.containsKey('monitor_pwd')) { + context.handle( + _monitorPwdMeta, + monitorPwd.isAcceptableOrUnknown(data['monitor_pwd']!, _monitorPwdMeta), + ); + } + if (data.containsKey('monitor_ignore_cert')) { + context.handle( + _monitorIgnoreCertMeta, + monitorIgnoreCert.isAcceptableOrUnknown( + data['monitor_ignore_cert']!, + _monitorIgnoreCertMeta, + ), + ); + } + if (data.containsKey('wol_mac')) { + context.handle( + _wolMacMeta, + wolMac.isAcceptableOrUnknown(data['wol_mac']!, _wolMacMeta), + ); + } + if (data.containsKey('wol_ip')) { + context.handle( + _wolIpMeta, + wolIp.isAcceptableOrUnknown(data['wol_ip']!, _wolIpMeta), + ); + } + if (data.containsKey('wol_pwd')) { + context.handle( + _wolPwdMeta, + wolPwd.isAcceptableOrUnknown(data['wol_pwd']!, _wolPwdMeta), + ); + } + if (data.containsKey('pve_addr')) { + context.handle( + _pveAddrMeta, + pveAddr.isAcceptableOrUnknown(data['pve_addr']!, _pveAddrMeta), + ); + } + if (data.containsKey('pve_ignore_cert')) { + context.handle( + _pveIgnoreCertMeta, + pveIgnoreCert.isAcceptableOrUnknown( + data['pve_ignore_cert']!, + _pveIgnoreCertMeta, + ), + ); + } + if (data.containsKey('pve_pwd')) { + context.handle( + _pvePwdMeta, + pvePwd.isAcceptableOrUnknown(data['pve_pwd']!, _pvePwdMeta), + ); + } + if (data.containsKey('prefer_temp_dev')) { + context.handle( + _preferTempDevMeta, + preferTempDev.isAcceptableOrUnknown( + data['prefer_temp_dev']!, + _preferTempDevMeta, + ), + ); + } + if (data.containsKey('temp_is_celsius')) { + context.handle( + _tempIsCelsiusMeta, + tempIsCelsius.isAcceptableOrUnknown( + data['temp_is_celsius']!, + _tempIsCelsiusMeta, + ), + ); + } + if (data.containsKey('logo_url')) { + context.handle( + _logoUrlMeta, + logoUrl.isAcceptableOrUnknown(data['logo_url']!, _logoUrlMeta), + ); + } + if (data.containsKey('net_dev')) { + context.handle( + _netDevMeta, + netDev.isAcceptableOrUnknown(data['net_dev']!, _netDevMeta), + ); + } + if (data.containsKey('script_dir')) { + context.handle( + _scriptDirMeta, + scriptDir.isAcceptableOrUnknown(data['script_dir']!, _scriptDirMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ServerRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ServerRow( + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + rev: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rev'], + )!, + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + autoConnect: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}auto_connect'], + )!, + systemType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}system_type'], + ), + sshIp: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ssh_ip'], + ), + sshPort: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}ssh_port'], + ), + sshUser: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ssh_user'], + ), + sshPwd: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ssh_pwd'], + ), + sshKeyId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ssh_key_id'], + ), + sshKeyPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ssh_key_path'], + ), + sshAlterUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ssh_alter_url'], + ), + sshProxyCommand: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ssh_proxy_command'], + ), + monitorAddr: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}monitor_addr'], + ), + monitorUser: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}monitor_user'], + ), + monitorPwd: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}monitor_pwd'], + ), + monitorIgnoreCert: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}monitor_ignore_cert'], + ), + wolMac: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}wol_mac'], + ), + wolIp: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}wol_ip'], + ), + wolPwd: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}wol_pwd'], + ), + pveAddr: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pve_addr'], + ), + pveIgnoreCert: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}pve_ignore_cert'], + )!, + pvePwd: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pve_pwd'], + ), + preferTempDev: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}prefer_temp_dev'], + ), + tempIsCelsius: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}temp_is_celsius'], + )!, + logoUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}logo_url'], + ), + netDev: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}net_dev'], + ), + scriptDir: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}script_dir'], + ), + ); + } + + @override + $ServersTable createAlias(String alias) { + return $ServersTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class ServerRow extends DataClass implements Insertable { + final int updatedAt; + final int rev; + final String id; + final String name; + final bool autoConnect; + final String? systemType; + final String? sshIp; + final int? sshPort; + final String? sshUser; + final String? sshPwd; + + /// Deleting a key must not delete the servers that used it; it must leave + /// them asking for a new one. + final String? sshKeyId; + final String? sshKeyPath; + final String? sshAlterUrl; + final String? sshProxyCommand; + final String? monitorAddr; + final String? monitorUser; + final String? monitorPwd; + final bool? monitorIgnoreCert; + final String? wolMac; + final String? wolIp; + final String? wolPwd; + final String? pveAddr; + final bool pveIgnoreCert; + final String? pvePwd; + final String? preferTempDev; + final bool tempIsCelsius; + final String? logoUrl; + final String? netDev; + final String? scriptDir; + const ServerRow({ + required this.updatedAt, + required this.rev, + required this.id, + required this.name, + required this.autoConnect, + this.systemType, + this.sshIp, + this.sshPort, + this.sshUser, + this.sshPwd, + this.sshKeyId, + this.sshKeyPath, + this.sshAlterUrl, + this.sshProxyCommand, + this.monitorAddr, + this.monitorUser, + this.monitorPwd, + this.monitorIgnoreCert, + this.wolMac, + this.wolIp, + this.wolPwd, + this.pveAddr, + required this.pveIgnoreCert, + this.pvePwd, + this.preferTempDev, + required this.tempIsCelsius, + this.logoUrl, + this.netDev, + this.scriptDir, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['updated_at'] = Variable(updatedAt); + map['rev'] = Variable(rev); + map['id'] = Variable(id); + map['name'] = Variable(name); + map['auto_connect'] = Variable(autoConnect); + if (!nullToAbsent || systemType != null) { + map['system_type'] = Variable(systemType); + } + if (!nullToAbsent || sshIp != null) { + map['ssh_ip'] = Variable(sshIp); + } + if (!nullToAbsent || sshPort != null) { + map['ssh_port'] = Variable(sshPort); + } + if (!nullToAbsent || sshUser != null) { + map['ssh_user'] = Variable(sshUser); + } + if (!nullToAbsent || sshPwd != null) { + map['ssh_pwd'] = Variable(sshPwd); + } + if (!nullToAbsent || sshKeyId != null) { + map['ssh_key_id'] = Variable(sshKeyId); + } + if (!nullToAbsent || sshKeyPath != null) { + map['ssh_key_path'] = Variable(sshKeyPath); + } + if (!nullToAbsent || sshAlterUrl != null) { + map['ssh_alter_url'] = Variable(sshAlterUrl); + } + if (!nullToAbsent || sshProxyCommand != null) { + map['ssh_proxy_command'] = Variable(sshProxyCommand); + } + if (!nullToAbsent || monitorAddr != null) { + map['monitor_addr'] = Variable(monitorAddr); + } + if (!nullToAbsent || monitorUser != null) { + map['monitor_user'] = Variable(monitorUser); + } + if (!nullToAbsent || monitorPwd != null) { + map['monitor_pwd'] = Variable(monitorPwd); + } + if (!nullToAbsent || monitorIgnoreCert != null) { + map['monitor_ignore_cert'] = Variable(monitorIgnoreCert); + } + if (!nullToAbsent || wolMac != null) { + map['wol_mac'] = Variable(wolMac); + } + if (!nullToAbsent || wolIp != null) { + map['wol_ip'] = Variable(wolIp); + } + if (!nullToAbsent || wolPwd != null) { + map['wol_pwd'] = Variable(wolPwd); + } + if (!nullToAbsent || pveAddr != null) { + map['pve_addr'] = Variable(pveAddr); + } + map['pve_ignore_cert'] = Variable(pveIgnoreCert); + if (!nullToAbsent || pvePwd != null) { + map['pve_pwd'] = Variable(pvePwd); + } + if (!nullToAbsent || preferTempDev != null) { + map['prefer_temp_dev'] = Variable(preferTempDev); + } + map['temp_is_celsius'] = Variable(tempIsCelsius); + if (!nullToAbsent || logoUrl != null) { + map['logo_url'] = Variable(logoUrl); + } + if (!nullToAbsent || netDev != null) { + map['net_dev'] = Variable(netDev); + } + if (!nullToAbsent || scriptDir != null) { + map['script_dir'] = Variable(scriptDir); + } + return map; + } + + ServersCompanion toCompanion(bool nullToAbsent) { + return ServersCompanion( + updatedAt: Value(updatedAt), + rev: Value(rev), + id: Value(id), + name: Value(name), + autoConnect: Value(autoConnect), + systemType: systemType == null && nullToAbsent + ? const Value.absent() + : Value(systemType), + sshIp: sshIp == null && nullToAbsent + ? const Value.absent() + : Value(sshIp), + sshPort: sshPort == null && nullToAbsent + ? const Value.absent() + : Value(sshPort), + sshUser: sshUser == null && nullToAbsent + ? const Value.absent() + : Value(sshUser), + sshPwd: sshPwd == null && nullToAbsent + ? const Value.absent() + : Value(sshPwd), + sshKeyId: sshKeyId == null && nullToAbsent + ? const Value.absent() + : Value(sshKeyId), + sshKeyPath: sshKeyPath == null && nullToAbsent + ? const Value.absent() + : Value(sshKeyPath), + sshAlterUrl: sshAlterUrl == null && nullToAbsent + ? const Value.absent() + : Value(sshAlterUrl), + sshProxyCommand: sshProxyCommand == null && nullToAbsent + ? const Value.absent() + : Value(sshProxyCommand), + monitorAddr: monitorAddr == null && nullToAbsent + ? const Value.absent() + : Value(monitorAddr), + monitorUser: monitorUser == null && nullToAbsent + ? const Value.absent() + : Value(monitorUser), + monitorPwd: monitorPwd == null && nullToAbsent + ? const Value.absent() + : Value(monitorPwd), + monitorIgnoreCert: monitorIgnoreCert == null && nullToAbsent + ? const Value.absent() + : Value(monitorIgnoreCert), + wolMac: wolMac == null && nullToAbsent + ? const Value.absent() + : Value(wolMac), + wolIp: wolIp == null && nullToAbsent + ? const Value.absent() + : Value(wolIp), + wolPwd: wolPwd == null && nullToAbsent + ? const Value.absent() + : Value(wolPwd), + pveAddr: pveAddr == null && nullToAbsent + ? const Value.absent() + : Value(pveAddr), + pveIgnoreCert: Value(pveIgnoreCert), + pvePwd: pvePwd == null && nullToAbsent + ? const Value.absent() + : Value(pvePwd), + preferTempDev: preferTempDev == null && nullToAbsent + ? const Value.absent() + : Value(preferTempDev), + tempIsCelsius: Value(tempIsCelsius), + logoUrl: logoUrl == null && nullToAbsent + ? const Value.absent() + : Value(logoUrl), + netDev: netDev == null && nullToAbsent + ? const Value.absent() + : Value(netDev), + scriptDir: scriptDir == null && nullToAbsent + ? const Value.absent() + : Value(scriptDir), + ); + } + + factory ServerRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ServerRow( + updatedAt: serializer.fromJson(json['updatedAt']), + rev: serializer.fromJson(json['rev']), + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + autoConnect: serializer.fromJson(json['autoConnect']), + systemType: serializer.fromJson(json['systemType']), + sshIp: serializer.fromJson(json['sshIp']), + sshPort: serializer.fromJson(json['sshPort']), + sshUser: serializer.fromJson(json['sshUser']), + sshPwd: serializer.fromJson(json['sshPwd']), + sshKeyId: serializer.fromJson(json['sshKeyId']), + sshKeyPath: serializer.fromJson(json['sshKeyPath']), + sshAlterUrl: serializer.fromJson(json['sshAlterUrl']), + sshProxyCommand: serializer.fromJson(json['sshProxyCommand']), + monitorAddr: serializer.fromJson(json['monitorAddr']), + monitorUser: serializer.fromJson(json['monitorUser']), + monitorPwd: serializer.fromJson(json['monitorPwd']), + monitorIgnoreCert: serializer.fromJson(json['monitorIgnoreCert']), + wolMac: serializer.fromJson(json['wolMac']), + wolIp: serializer.fromJson(json['wolIp']), + wolPwd: serializer.fromJson(json['wolPwd']), + pveAddr: serializer.fromJson(json['pveAddr']), + pveIgnoreCert: serializer.fromJson(json['pveIgnoreCert']), + pvePwd: serializer.fromJson(json['pvePwd']), + preferTempDev: serializer.fromJson(json['preferTempDev']), + tempIsCelsius: serializer.fromJson(json['tempIsCelsius']), + logoUrl: serializer.fromJson(json['logoUrl']), + netDev: serializer.fromJson(json['netDev']), + scriptDir: serializer.fromJson(json['scriptDir']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'updatedAt': serializer.toJson(updatedAt), + 'rev': serializer.toJson(rev), + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'autoConnect': serializer.toJson(autoConnect), + 'systemType': serializer.toJson(systemType), + 'sshIp': serializer.toJson(sshIp), + 'sshPort': serializer.toJson(sshPort), + 'sshUser': serializer.toJson(sshUser), + 'sshPwd': serializer.toJson(sshPwd), + 'sshKeyId': serializer.toJson(sshKeyId), + 'sshKeyPath': serializer.toJson(sshKeyPath), + 'sshAlterUrl': serializer.toJson(sshAlterUrl), + 'sshProxyCommand': serializer.toJson(sshProxyCommand), + 'monitorAddr': serializer.toJson(monitorAddr), + 'monitorUser': serializer.toJson(monitorUser), + 'monitorPwd': serializer.toJson(monitorPwd), + 'monitorIgnoreCert': serializer.toJson(monitorIgnoreCert), + 'wolMac': serializer.toJson(wolMac), + 'wolIp': serializer.toJson(wolIp), + 'wolPwd': serializer.toJson(wolPwd), + 'pveAddr': serializer.toJson(pveAddr), + 'pveIgnoreCert': serializer.toJson(pveIgnoreCert), + 'pvePwd': serializer.toJson(pvePwd), + 'preferTempDev': serializer.toJson(preferTempDev), + 'tempIsCelsius': serializer.toJson(tempIsCelsius), + 'logoUrl': serializer.toJson(logoUrl), + 'netDev': serializer.toJson(netDev), + 'scriptDir': serializer.toJson(scriptDir), + }; + } + + ServerRow copyWith({ + int? updatedAt, + int? rev, + String? id, + String? name, + bool? autoConnect, + Value systemType = const Value.absent(), + Value sshIp = const Value.absent(), + Value sshPort = const Value.absent(), + Value sshUser = const Value.absent(), + Value sshPwd = const Value.absent(), + Value sshKeyId = const Value.absent(), + Value sshKeyPath = const Value.absent(), + Value sshAlterUrl = const Value.absent(), + Value sshProxyCommand = const Value.absent(), + Value monitorAddr = const Value.absent(), + Value monitorUser = const Value.absent(), + Value monitorPwd = const Value.absent(), + Value monitorIgnoreCert = const Value.absent(), + Value wolMac = const Value.absent(), + Value wolIp = const Value.absent(), + Value wolPwd = const Value.absent(), + Value pveAddr = const Value.absent(), + bool? pveIgnoreCert, + Value pvePwd = const Value.absent(), + Value preferTempDev = const Value.absent(), + bool? tempIsCelsius, + Value logoUrl = const Value.absent(), + Value netDev = const Value.absent(), + Value scriptDir = const Value.absent(), + }) => ServerRow( + updatedAt: updatedAt ?? this.updatedAt, + rev: rev ?? this.rev, + id: id ?? this.id, + name: name ?? this.name, + autoConnect: autoConnect ?? this.autoConnect, + systemType: systemType.present ? systemType.value : this.systemType, + sshIp: sshIp.present ? sshIp.value : this.sshIp, + sshPort: sshPort.present ? sshPort.value : this.sshPort, + sshUser: sshUser.present ? sshUser.value : this.sshUser, + sshPwd: sshPwd.present ? sshPwd.value : this.sshPwd, + sshKeyId: sshKeyId.present ? sshKeyId.value : this.sshKeyId, + sshKeyPath: sshKeyPath.present ? sshKeyPath.value : this.sshKeyPath, + sshAlterUrl: sshAlterUrl.present ? sshAlterUrl.value : this.sshAlterUrl, + sshProxyCommand: sshProxyCommand.present + ? sshProxyCommand.value + : this.sshProxyCommand, + monitorAddr: monitorAddr.present ? monitorAddr.value : this.monitorAddr, + monitorUser: monitorUser.present ? monitorUser.value : this.monitorUser, + monitorPwd: monitorPwd.present ? monitorPwd.value : this.monitorPwd, + monitorIgnoreCert: monitorIgnoreCert.present + ? monitorIgnoreCert.value + : this.monitorIgnoreCert, + wolMac: wolMac.present ? wolMac.value : this.wolMac, + wolIp: wolIp.present ? wolIp.value : this.wolIp, + wolPwd: wolPwd.present ? wolPwd.value : this.wolPwd, + pveAddr: pveAddr.present ? pveAddr.value : this.pveAddr, + pveIgnoreCert: pveIgnoreCert ?? this.pveIgnoreCert, + pvePwd: pvePwd.present ? pvePwd.value : this.pvePwd, + preferTempDev: preferTempDev.present + ? preferTempDev.value + : this.preferTempDev, + tempIsCelsius: tempIsCelsius ?? this.tempIsCelsius, + logoUrl: logoUrl.present ? logoUrl.value : this.logoUrl, + netDev: netDev.present ? netDev.value : this.netDev, + scriptDir: scriptDir.present ? scriptDir.value : this.scriptDir, + ); + ServerRow copyWithCompanion(ServersCompanion data) { + return ServerRow( + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + rev: data.rev.present ? data.rev.value : this.rev, + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + autoConnect: data.autoConnect.present + ? data.autoConnect.value + : this.autoConnect, + systemType: data.systemType.present + ? data.systemType.value + : this.systemType, + sshIp: data.sshIp.present ? data.sshIp.value : this.sshIp, + sshPort: data.sshPort.present ? data.sshPort.value : this.sshPort, + sshUser: data.sshUser.present ? data.sshUser.value : this.sshUser, + sshPwd: data.sshPwd.present ? data.sshPwd.value : this.sshPwd, + sshKeyId: data.sshKeyId.present ? data.sshKeyId.value : this.sshKeyId, + sshKeyPath: data.sshKeyPath.present + ? data.sshKeyPath.value + : this.sshKeyPath, + sshAlterUrl: data.sshAlterUrl.present + ? data.sshAlterUrl.value + : this.sshAlterUrl, + sshProxyCommand: data.sshProxyCommand.present + ? data.sshProxyCommand.value + : this.sshProxyCommand, + monitorAddr: data.monitorAddr.present + ? data.monitorAddr.value + : this.monitorAddr, + monitorUser: data.monitorUser.present + ? data.monitorUser.value + : this.monitorUser, + monitorPwd: data.monitorPwd.present + ? data.monitorPwd.value + : this.monitorPwd, + monitorIgnoreCert: data.monitorIgnoreCert.present + ? data.monitorIgnoreCert.value + : this.monitorIgnoreCert, + wolMac: data.wolMac.present ? data.wolMac.value : this.wolMac, + wolIp: data.wolIp.present ? data.wolIp.value : this.wolIp, + wolPwd: data.wolPwd.present ? data.wolPwd.value : this.wolPwd, + pveAddr: data.pveAddr.present ? data.pveAddr.value : this.pveAddr, + pveIgnoreCert: data.pveIgnoreCert.present + ? data.pveIgnoreCert.value + : this.pveIgnoreCert, + pvePwd: data.pvePwd.present ? data.pvePwd.value : this.pvePwd, + preferTempDev: data.preferTempDev.present + ? data.preferTempDev.value + : this.preferTempDev, + tempIsCelsius: data.tempIsCelsius.present + ? data.tempIsCelsius.value + : this.tempIsCelsius, + logoUrl: data.logoUrl.present ? data.logoUrl.value : this.logoUrl, + netDev: data.netDev.present ? data.netDev.value : this.netDev, + scriptDir: data.scriptDir.present ? data.scriptDir.value : this.scriptDir, + ); + } + + @override + String toString() { + return (StringBuffer('ServerRow(') + ..write('updatedAt: $updatedAt, ') + ..write('rev: $rev, ') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('autoConnect: $autoConnect, ') + ..write('systemType: $systemType, ') + ..write('sshIp: $sshIp, ') + ..write('sshPort: $sshPort, ') + ..write('sshUser: $sshUser, ') + ..write('sshPwd: $sshPwd, ') + ..write('sshKeyId: $sshKeyId, ') + ..write('sshKeyPath: $sshKeyPath, ') + ..write('sshAlterUrl: $sshAlterUrl, ') + ..write('sshProxyCommand: $sshProxyCommand, ') + ..write('monitorAddr: $monitorAddr, ') + ..write('monitorUser: $monitorUser, ') + ..write('monitorPwd: $monitorPwd, ') + ..write('monitorIgnoreCert: $monitorIgnoreCert, ') + ..write('wolMac: $wolMac, ') + ..write('wolIp: $wolIp, ') + ..write('wolPwd: $wolPwd, ') + ..write('pveAddr: $pveAddr, ') + ..write('pveIgnoreCert: $pveIgnoreCert, ') + ..write('pvePwd: $pvePwd, ') + ..write('preferTempDev: $preferTempDev, ') + ..write('tempIsCelsius: $tempIsCelsius, ') + ..write('logoUrl: $logoUrl, ') + ..write('netDev: $netDev, ') + ..write('scriptDir: $scriptDir') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + updatedAt, + rev, + id, + name, + autoConnect, + systemType, + sshIp, + sshPort, + sshUser, + sshPwd, + sshKeyId, + sshKeyPath, + sshAlterUrl, + sshProxyCommand, + monitorAddr, + monitorUser, + monitorPwd, + monitorIgnoreCert, + wolMac, + wolIp, + wolPwd, + pveAddr, + pveIgnoreCert, + pvePwd, + preferTempDev, + tempIsCelsius, + logoUrl, + netDev, + scriptDir, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ServerRow && + other.updatedAt == this.updatedAt && + other.rev == this.rev && + other.id == this.id && + other.name == this.name && + other.autoConnect == this.autoConnect && + other.systemType == this.systemType && + other.sshIp == this.sshIp && + other.sshPort == this.sshPort && + other.sshUser == this.sshUser && + other.sshPwd == this.sshPwd && + other.sshKeyId == this.sshKeyId && + other.sshKeyPath == this.sshKeyPath && + other.sshAlterUrl == this.sshAlterUrl && + other.sshProxyCommand == this.sshProxyCommand && + other.monitorAddr == this.monitorAddr && + other.monitorUser == this.monitorUser && + other.monitorPwd == this.monitorPwd && + other.monitorIgnoreCert == this.monitorIgnoreCert && + other.wolMac == this.wolMac && + other.wolIp == this.wolIp && + other.wolPwd == this.wolPwd && + other.pveAddr == this.pveAddr && + other.pveIgnoreCert == this.pveIgnoreCert && + other.pvePwd == this.pvePwd && + other.preferTempDev == this.preferTempDev && + other.tempIsCelsius == this.tempIsCelsius && + other.logoUrl == this.logoUrl && + other.netDev == this.netDev && + other.scriptDir == this.scriptDir); +} + +class ServersCompanion extends UpdateCompanion { + final Value updatedAt; + final Value rev; + final Value id; + final Value name; + final Value autoConnect; + final Value systemType; + final Value sshIp; + final Value sshPort; + final Value sshUser; + final Value sshPwd; + final Value sshKeyId; + final Value sshKeyPath; + final Value sshAlterUrl; + final Value sshProxyCommand; + final Value monitorAddr; + final Value monitorUser; + final Value monitorPwd; + final Value monitorIgnoreCert; + final Value wolMac; + final Value wolIp; + final Value wolPwd; + final Value pveAddr; + final Value pveIgnoreCert; + final Value pvePwd; + final Value preferTempDev; + final Value tempIsCelsius; + final Value logoUrl; + final Value netDev; + final Value scriptDir; + const ServersCompanion({ + this.updatedAt = const Value.absent(), + this.rev = const Value.absent(), + this.id = const Value.absent(), + this.name = const Value.absent(), + this.autoConnect = const Value.absent(), + this.systemType = const Value.absent(), + this.sshIp = const Value.absent(), + this.sshPort = const Value.absent(), + this.sshUser = const Value.absent(), + this.sshPwd = const Value.absent(), + this.sshKeyId = const Value.absent(), + this.sshKeyPath = const Value.absent(), + this.sshAlterUrl = const Value.absent(), + this.sshProxyCommand = const Value.absent(), + this.monitorAddr = const Value.absent(), + this.monitorUser = const Value.absent(), + this.monitorPwd = const Value.absent(), + this.monitorIgnoreCert = const Value.absent(), + this.wolMac = const Value.absent(), + this.wolIp = const Value.absent(), + this.wolPwd = const Value.absent(), + this.pveAddr = const Value.absent(), + this.pveIgnoreCert = const Value.absent(), + this.pvePwd = const Value.absent(), + this.preferTempDev = const Value.absent(), + this.tempIsCelsius = const Value.absent(), + this.logoUrl = const Value.absent(), + this.netDev = const Value.absent(), + this.scriptDir = const Value.absent(), + }); + ServersCompanion.insert({ + this.updatedAt = const Value.absent(), + this.rev = const Value.absent(), + required String id, + required String name, + this.autoConnect = const Value.absent(), + this.systemType = const Value.absent(), + this.sshIp = const Value.absent(), + this.sshPort = const Value.absent(), + this.sshUser = const Value.absent(), + this.sshPwd = const Value.absent(), + this.sshKeyId = const Value.absent(), + this.sshKeyPath = const Value.absent(), + this.sshAlterUrl = const Value.absent(), + this.sshProxyCommand = const Value.absent(), + this.monitorAddr = const Value.absent(), + this.monitorUser = const Value.absent(), + this.monitorPwd = const Value.absent(), + this.monitorIgnoreCert = const Value.absent(), + this.wolMac = const Value.absent(), + this.wolIp = const Value.absent(), + this.wolPwd = const Value.absent(), + this.pveAddr = const Value.absent(), + this.pveIgnoreCert = const Value.absent(), + this.pvePwd = const Value.absent(), + this.preferTempDev = const Value.absent(), + this.tempIsCelsius = const Value.absent(), + this.logoUrl = const Value.absent(), + this.netDev = const Value.absent(), + this.scriptDir = const Value.absent(), + }) : id = Value(id), + name = Value(name); + static Insertable custom({ + Expression? updatedAt, + Expression? rev, + Expression? id, + Expression? name, + Expression? autoConnect, + Expression? systemType, + Expression? sshIp, + Expression? sshPort, + Expression? sshUser, + Expression? sshPwd, + Expression? sshKeyId, + Expression? sshKeyPath, + Expression? sshAlterUrl, + Expression? sshProxyCommand, + Expression? monitorAddr, + Expression? monitorUser, + Expression? monitorPwd, + Expression? monitorIgnoreCert, + Expression? wolMac, + Expression? wolIp, + Expression? wolPwd, + Expression? pveAddr, + Expression? pveIgnoreCert, + Expression? pvePwd, + Expression? preferTempDev, + Expression? tempIsCelsius, + Expression? logoUrl, + Expression? netDev, + Expression? scriptDir, + }) { + return RawValuesInsertable({ + if (updatedAt != null) 'updated_at': updatedAt, + if (rev != null) 'rev': rev, + if (id != null) 'id': id, + if (name != null) 'name': name, + if (autoConnect != null) 'auto_connect': autoConnect, + if (systemType != null) 'system_type': systemType, + if (sshIp != null) 'ssh_ip': sshIp, + if (sshPort != null) 'ssh_port': sshPort, + if (sshUser != null) 'ssh_user': sshUser, + if (sshPwd != null) 'ssh_pwd': sshPwd, + if (sshKeyId != null) 'ssh_key_id': sshKeyId, + if (sshKeyPath != null) 'ssh_key_path': sshKeyPath, + if (sshAlterUrl != null) 'ssh_alter_url': sshAlterUrl, + if (sshProxyCommand != null) 'ssh_proxy_command': sshProxyCommand, + if (monitorAddr != null) 'monitor_addr': monitorAddr, + if (monitorUser != null) 'monitor_user': monitorUser, + if (monitorPwd != null) 'monitor_pwd': monitorPwd, + if (monitorIgnoreCert != null) 'monitor_ignore_cert': monitorIgnoreCert, + if (wolMac != null) 'wol_mac': wolMac, + if (wolIp != null) 'wol_ip': wolIp, + if (wolPwd != null) 'wol_pwd': wolPwd, + if (pveAddr != null) 'pve_addr': pveAddr, + if (pveIgnoreCert != null) 'pve_ignore_cert': pveIgnoreCert, + if (pvePwd != null) 'pve_pwd': pvePwd, + if (preferTempDev != null) 'prefer_temp_dev': preferTempDev, + if (tempIsCelsius != null) 'temp_is_celsius': tempIsCelsius, + if (logoUrl != null) 'logo_url': logoUrl, + if (netDev != null) 'net_dev': netDev, + if (scriptDir != null) 'script_dir': scriptDir, + }); + } + + ServersCompanion copyWith({ + Value? updatedAt, + Value? rev, + Value? id, + Value? name, + Value? autoConnect, + Value? systemType, + Value? sshIp, + Value? sshPort, + Value? sshUser, + Value? sshPwd, + Value? sshKeyId, + Value? sshKeyPath, + Value? sshAlterUrl, + Value? sshProxyCommand, + Value? monitorAddr, + Value? monitorUser, + Value? monitorPwd, + Value? monitorIgnoreCert, + Value? wolMac, + Value? wolIp, + Value? wolPwd, + Value? pveAddr, + Value? pveIgnoreCert, + Value? pvePwd, + Value? preferTempDev, + Value? tempIsCelsius, + Value? logoUrl, + Value? netDev, + Value? scriptDir, + }) { + return ServersCompanion( + updatedAt: updatedAt ?? this.updatedAt, + rev: rev ?? this.rev, + id: id ?? this.id, + name: name ?? this.name, + autoConnect: autoConnect ?? this.autoConnect, + systemType: systemType ?? this.systemType, + sshIp: sshIp ?? this.sshIp, + sshPort: sshPort ?? this.sshPort, + sshUser: sshUser ?? this.sshUser, + sshPwd: sshPwd ?? this.sshPwd, + sshKeyId: sshKeyId ?? this.sshKeyId, + sshKeyPath: sshKeyPath ?? this.sshKeyPath, + sshAlterUrl: sshAlterUrl ?? this.sshAlterUrl, + sshProxyCommand: sshProxyCommand ?? this.sshProxyCommand, + monitorAddr: monitorAddr ?? this.monitorAddr, + monitorUser: monitorUser ?? this.monitorUser, + monitorPwd: monitorPwd ?? this.monitorPwd, + monitorIgnoreCert: monitorIgnoreCert ?? this.monitorIgnoreCert, + wolMac: wolMac ?? this.wolMac, + wolIp: wolIp ?? this.wolIp, + wolPwd: wolPwd ?? this.wolPwd, + pveAddr: pveAddr ?? this.pveAddr, + pveIgnoreCert: pveIgnoreCert ?? this.pveIgnoreCert, + pvePwd: pvePwd ?? this.pvePwd, + preferTempDev: preferTempDev ?? this.preferTempDev, + tempIsCelsius: tempIsCelsius ?? this.tempIsCelsius, + logoUrl: logoUrl ?? this.logoUrl, + netDev: netDev ?? this.netDev, + scriptDir: scriptDir ?? this.scriptDir, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (autoConnect.present) { + map['auto_connect'] = Variable(autoConnect.value); + } + if (systemType.present) { + map['system_type'] = Variable(systemType.value); + } + if (sshIp.present) { + map['ssh_ip'] = Variable(sshIp.value); + } + if (sshPort.present) { + map['ssh_port'] = Variable(sshPort.value); + } + if (sshUser.present) { + map['ssh_user'] = Variable(sshUser.value); + } + if (sshPwd.present) { + map['ssh_pwd'] = Variable(sshPwd.value); + } + if (sshKeyId.present) { + map['ssh_key_id'] = Variable(sshKeyId.value); + } + if (sshKeyPath.present) { + map['ssh_key_path'] = Variable(sshKeyPath.value); + } + if (sshAlterUrl.present) { + map['ssh_alter_url'] = Variable(sshAlterUrl.value); + } + if (sshProxyCommand.present) { + map['ssh_proxy_command'] = Variable(sshProxyCommand.value); + } + if (monitorAddr.present) { + map['monitor_addr'] = Variable(monitorAddr.value); + } + if (monitorUser.present) { + map['monitor_user'] = Variable(monitorUser.value); + } + if (monitorPwd.present) { + map['monitor_pwd'] = Variable(monitorPwd.value); + } + if (monitorIgnoreCert.present) { + map['monitor_ignore_cert'] = Variable(monitorIgnoreCert.value); + } + if (wolMac.present) { + map['wol_mac'] = Variable(wolMac.value); + } + if (wolIp.present) { + map['wol_ip'] = Variable(wolIp.value); + } + if (wolPwd.present) { + map['wol_pwd'] = Variable(wolPwd.value); + } + if (pveAddr.present) { + map['pve_addr'] = Variable(pveAddr.value); + } + if (pveIgnoreCert.present) { + map['pve_ignore_cert'] = Variable(pveIgnoreCert.value); + } + if (pvePwd.present) { + map['pve_pwd'] = Variable(pvePwd.value); + } + if (preferTempDev.present) { + map['prefer_temp_dev'] = Variable(preferTempDev.value); + } + if (tempIsCelsius.present) { + map['temp_is_celsius'] = Variable(tempIsCelsius.value); + } + if (logoUrl.present) { + map['logo_url'] = Variable(logoUrl.value); + } + if (netDev.present) { + map['net_dev'] = Variable(netDev.value); + } + if (scriptDir.present) { + map['script_dir'] = Variable(scriptDir.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ServersCompanion(') + ..write('updatedAt: $updatedAt, ') + ..write('rev: $rev, ') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('autoConnect: $autoConnect, ') + ..write('systemType: $systemType, ') + ..write('sshIp: $sshIp, ') + ..write('sshPort: $sshPort, ') + ..write('sshUser: $sshUser, ') + ..write('sshPwd: $sshPwd, ') + ..write('sshKeyId: $sshKeyId, ') + ..write('sshKeyPath: $sshKeyPath, ') + ..write('sshAlterUrl: $sshAlterUrl, ') + ..write('sshProxyCommand: $sshProxyCommand, ') + ..write('monitorAddr: $monitorAddr, ') + ..write('monitorUser: $monitorUser, ') + ..write('monitorPwd: $monitorPwd, ') + ..write('monitorIgnoreCert: $monitorIgnoreCert, ') + ..write('wolMac: $wolMac, ') + ..write('wolIp: $wolIp, ') + ..write('wolPwd: $wolPwd, ') + ..write('pveAddr: $pveAddr, ') + ..write('pveIgnoreCert: $pveIgnoreCert, ') + ..write('pvePwd: $pvePwd, ') + ..write('preferTempDev: $preferTempDev, ') + ..write('tempIsCelsius: $tempIsCelsius, ') + ..write('logoUrl: $logoUrl, ') + ..write('netDev: $netDev, ') + ..write('scriptDir: $scriptDir') + ..write(')')) + .toString(); + } +} + +class $ServerTagsTable extends ServerTags + with TableInfo<$ServerTagsTable, ServerTagRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ServerTagsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _tagMeta = const VerificationMeta('tag'); + @override + late final GeneratedColumn tag = GeneratedColumn( + 'tag', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [serverId, tag]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'server_tag'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('tag')) { + context.handle( + _tagMeta, + tag.isAcceptableOrUnknown(data['tag']!, _tagMeta), + ); + } else if (isInserting) { + context.missing(_tagMeta); + } + return context; + } + + @override + Set get $primaryKey => {serverId, tag}; + @override + ServerTagRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ServerTagRow( + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + tag: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}tag'], + )!, + ); + } + + @override + $ServerTagsTable createAlias(String alias) { + return $ServerTagsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class ServerTagRow extends DataClass implements Insertable { + final String serverId; + final String tag; + const ServerTagRow({required this.serverId, required this.tag}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['server_id'] = Variable(serverId); + map['tag'] = Variable(tag); + return map; + } + + ServerTagsCompanion toCompanion(bool nullToAbsent) { + return ServerTagsCompanion(serverId: Value(serverId), tag: Value(tag)); + } + + factory ServerTagRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ServerTagRow( + serverId: serializer.fromJson(json['serverId']), + tag: serializer.fromJson(json['tag']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'serverId': serializer.toJson(serverId), + 'tag': serializer.toJson(tag), + }; + } + + ServerTagRow copyWith({String? serverId, String? tag}) => + ServerTagRow(serverId: serverId ?? this.serverId, tag: tag ?? this.tag); + ServerTagRow copyWithCompanion(ServerTagsCompanion data) { + return ServerTagRow( + serverId: data.serverId.present ? data.serverId.value : this.serverId, + tag: data.tag.present ? data.tag.value : this.tag, + ); + } + + @override + String toString() { + return (StringBuffer('ServerTagRow(') + ..write('serverId: $serverId, ') + ..write('tag: $tag') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(serverId, tag); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ServerTagRow && + other.serverId == this.serverId && + other.tag == this.tag); +} + +class ServerTagsCompanion extends UpdateCompanion { + final Value serverId; + final Value tag; + const ServerTagsCompanion({ + this.serverId = const Value.absent(), + this.tag = const Value.absent(), + }); + ServerTagsCompanion.insert({required String serverId, required String tag}) + : serverId = Value(serverId), + tag = Value(tag); + static Insertable custom({ + Expression? serverId, + Expression? tag, + }) { + return RawValuesInsertable({ + if (serverId != null) 'server_id': serverId, + if (tag != null) 'tag': tag, + }); + } + + ServerTagsCompanion copyWith({Value? serverId, Value? tag}) { + return ServerTagsCompanion( + serverId: serverId ?? this.serverId, + tag: tag ?? this.tag, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (tag.present) { + map['tag'] = Variable(tag.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ServerTagsCompanion(') + ..write('serverId: $serverId, ') + ..write('tag: $tag') + ..write(')')) + .toString(); + } +} + +class $ServerEnvsTable extends ServerEnvs + with TableInfo<$ServerEnvsTable, ServerEnvRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ServerEnvsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _keyMeta = const VerificationMeta('key'); + @override + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _valueMeta = const VerificationMeta('value'); + @override + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [serverId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'server_env'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('key')) { + context.handle( + _keyMeta, + key.isAcceptableOrUnknown(data['key']!, _keyMeta), + ); + } else if (isInserting) { + context.missing(_keyMeta); + } + if (data.containsKey('value')) { + context.handle( + _valueMeta, + value.isAcceptableOrUnknown(data['value']!, _valueMeta), + ); + } else if (isInserting) { + context.missing(_valueMeta); + } + return context; + } + + @override + Set get $primaryKey => {serverId, key}; + @override + ServerEnvRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ServerEnvRow( + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + $ServerEnvsTable createAlias(String alias) { + return $ServerEnvsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class ServerEnvRow extends DataClass implements Insertable { + final String serverId; + final String key; + final String value; + const ServerEnvRow({ + required this.serverId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['server_id'] = Variable(serverId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + ServerEnvsCompanion toCompanion(bool nullToAbsent) { + return ServerEnvsCompanion( + serverId: Value(serverId), + key: Value(key), + value: Value(value), + ); + } + + factory ServerEnvRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ServerEnvRow( + serverId: serializer.fromJson(json['serverId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'serverId': serializer.toJson(serverId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + ServerEnvRow copyWith({String? serverId, String? key, String? value}) => + ServerEnvRow( + serverId: serverId ?? this.serverId, + key: key ?? this.key, + value: value ?? this.value, + ); + ServerEnvRow copyWithCompanion(ServerEnvsCompanion data) { + return ServerEnvRow( + serverId: data.serverId.present ? data.serverId.value : this.serverId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('ServerEnvRow(') + ..write('serverId: $serverId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(serverId, key, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ServerEnvRow && + other.serverId == this.serverId && + other.key == this.key && + other.value == this.value); +} + +class ServerEnvsCompanion extends UpdateCompanion { + final Value serverId; + final Value key; + final Value value; + const ServerEnvsCompanion({ + this.serverId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + ServerEnvsCompanion.insert({ + required String serverId, + required String key, + required String value, + }) : serverId = Value(serverId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? serverId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (serverId != null) 'server_id': serverId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + ServerEnvsCompanion copyWith({ + Value? serverId, + Value? key, + Value? value, + }) { + return ServerEnvsCompanion( + serverId: serverId ?? this.serverId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ServerEnvsCompanion(') + ..write('serverId: $serverId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class $ServerJumpsTable extends ServerJumps + with TableInfo<$ServerJumpsTable, ServerJumpRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ServerJumpsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _ordMeta = const VerificationMeta('ord'); + @override + late final GeneratedColumn ord = GeneratedColumn( + 'ord', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _jumpIdMeta = const VerificationMeta('jumpId'); + @override + late final GeneratedColumn jumpId = GeneratedColumn( + 'jump_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [serverId, ord, jumpId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'server_jump'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('ord')) { + context.handle( + _ordMeta, + ord.isAcceptableOrUnknown(data['ord']!, _ordMeta), + ); + } else if (isInserting) { + context.missing(_ordMeta); + } + if (data.containsKey('jump_id')) { + context.handle( + _jumpIdMeta, + jumpId.isAcceptableOrUnknown(data['jump_id']!, _jumpIdMeta), + ); + } else if (isInserting) { + context.missing(_jumpIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {serverId, ord}; + @override + ServerJumpRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ServerJumpRow( + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + ord: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}ord'], + )!, + jumpId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}jump_id'], + )!, + ); + } + + @override + $ServerJumpsTable createAlias(String alias) { + return $ServerJumpsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class ServerJumpRow extends DataClass implements Insertable { + final String serverId; + final int ord; + final String jumpId; + const ServerJumpRow({ + required this.serverId, + required this.ord, + required this.jumpId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['server_id'] = Variable(serverId); + map['ord'] = Variable(ord); + map['jump_id'] = Variable(jumpId); + return map; + } + + ServerJumpsCompanion toCompanion(bool nullToAbsent) { + return ServerJumpsCompanion( + serverId: Value(serverId), + ord: Value(ord), + jumpId: Value(jumpId), + ); + } + + factory ServerJumpRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ServerJumpRow( + serverId: serializer.fromJson(json['serverId']), + ord: serializer.fromJson(json['ord']), + jumpId: serializer.fromJson(json['jumpId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'serverId': serializer.toJson(serverId), + 'ord': serializer.toJson(ord), + 'jumpId': serializer.toJson(jumpId), + }; + } + + ServerJumpRow copyWith({String? serverId, int? ord, String? jumpId}) => + ServerJumpRow( + serverId: serverId ?? this.serverId, + ord: ord ?? this.ord, + jumpId: jumpId ?? this.jumpId, + ); + ServerJumpRow copyWithCompanion(ServerJumpsCompanion data) { + return ServerJumpRow( + serverId: data.serverId.present ? data.serverId.value : this.serverId, + ord: data.ord.present ? data.ord.value : this.ord, + jumpId: data.jumpId.present ? data.jumpId.value : this.jumpId, + ); + } + + @override + String toString() { + return (StringBuffer('ServerJumpRow(') + ..write('serverId: $serverId, ') + ..write('ord: $ord, ') + ..write('jumpId: $jumpId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(serverId, ord, jumpId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ServerJumpRow && + other.serverId == this.serverId && + other.ord == this.ord && + other.jumpId == this.jumpId); +} + +class ServerJumpsCompanion extends UpdateCompanion { + final Value serverId; + final Value ord; + final Value jumpId; + const ServerJumpsCompanion({ + this.serverId = const Value.absent(), + this.ord = const Value.absent(), + this.jumpId = const Value.absent(), + }); + ServerJumpsCompanion.insert({ + required String serverId, + required int ord, + required String jumpId, + }) : serverId = Value(serverId), + ord = Value(ord), + jumpId = Value(jumpId); + static Insertable custom({ + Expression? serverId, + Expression? ord, + Expression? jumpId, + }) { + return RawValuesInsertable({ + if (serverId != null) 'server_id': serverId, + if (ord != null) 'ord': ord, + if (jumpId != null) 'jump_id': jumpId, + }); + } + + ServerJumpsCompanion copyWith({ + Value? serverId, + Value? ord, + Value? jumpId, + }) { + return ServerJumpsCompanion( + serverId: serverId ?? this.serverId, + ord: ord ?? this.ord, + jumpId: jumpId ?? this.jumpId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (ord.present) { + map['ord'] = Variable(ord.value); + } + if (jumpId.present) { + map['jump_id'] = Variable(jumpId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ServerJumpsCompanion(') + ..write('serverId: $serverId, ') + ..write('ord: $ord, ') + ..write('jumpId: $jumpId') + ..write(')')) + .toString(); + } +} + +class $ServerDisabledCmdsTable extends ServerDisabledCmds + with TableInfo<$ServerDisabledCmdsTable, ServerDisabledCmdRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ServerDisabledCmdsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _cmdTypeMeta = const VerificationMeta( + 'cmdType', + ); + @override + late final GeneratedColumn cmdType = GeneratedColumn( + 'cmd_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [serverId, cmdType]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'server_disabled_cmd'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('cmd_type')) { + context.handle( + _cmdTypeMeta, + cmdType.isAcceptableOrUnknown(data['cmd_type']!, _cmdTypeMeta), + ); + } else if (isInserting) { + context.missing(_cmdTypeMeta); + } + return context; + } + + @override + Set get $primaryKey => {serverId, cmdType}; + @override + ServerDisabledCmdRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ServerDisabledCmdRow( + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + cmdType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cmd_type'], + )!, + ); + } + + @override + $ServerDisabledCmdsTable createAlias(String alias) { + return $ServerDisabledCmdsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class ServerDisabledCmdRow extends DataClass + implements Insertable { + final String serverId; + final String cmdType; + const ServerDisabledCmdRow({required this.serverId, required this.cmdType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['server_id'] = Variable(serverId); + map['cmd_type'] = Variable(cmdType); + return map; + } + + ServerDisabledCmdsCompanion toCompanion(bool nullToAbsent) { + return ServerDisabledCmdsCompanion( + serverId: Value(serverId), + cmdType: Value(cmdType), + ); + } + + factory ServerDisabledCmdRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ServerDisabledCmdRow( + serverId: serializer.fromJson(json['serverId']), + cmdType: serializer.fromJson(json['cmdType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'serverId': serializer.toJson(serverId), + 'cmdType': serializer.toJson(cmdType), + }; + } + + ServerDisabledCmdRow copyWith({String? serverId, String? cmdType}) => + ServerDisabledCmdRow( + serverId: serverId ?? this.serverId, + cmdType: cmdType ?? this.cmdType, + ); + ServerDisabledCmdRow copyWithCompanion(ServerDisabledCmdsCompanion data) { + return ServerDisabledCmdRow( + serverId: data.serverId.present ? data.serverId.value : this.serverId, + cmdType: data.cmdType.present ? data.cmdType.value : this.cmdType, + ); + } + + @override + String toString() { + return (StringBuffer('ServerDisabledCmdRow(') + ..write('serverId: $serverId, ') + ..write('cmdType: $cmdType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(serverId, cmdType); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ServerDisabledCmdRow && + other.serverId == this.serverId && + other.cmdType == this.cmdType); +} + +class ServerDisabledCmdsCompanion + extends UpdateCompanion { + final Value serverId; + final Value cmdType; + const ServerDisabledCmdsCompanion({ + this.serverId = const Value.absent(), + this.cmdType = const Value.absent(), + }); + ServerDisabledCmdsCompanion.insert({ + required String serverId, + required String cmdType, + }) : serverId = Value(serverId), + cmdType = Value(cmdType); + static Insertable custom({ + Expression? serverId, + Expression? cmdType, + }) { + return RawValuesInsertable({ + if (serverId != null) 'server_id': serverId, + if (cmdType != null) 'cmd_type': cmdType, + }); + } + + ServerDisabledCmdsCompanion copyWith({ + Value? serverId, + Value? cmdType, + }) { + return ServerDisabledCmdsCompanion( + serverId: serverId ?? this.serverId, + cmdType: cmdType ?? this.cmdType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (cmdType.present) { + map['cmd_type'] = Variable(cmdType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ServerDisabledCmdsCompanion(') + ..write('serverId: $serverId, ') + ..write('cmdType: $cmdType') + ..write(')')) + .toString(); + } +} + +class $ServerCustomCmdsTable extends ServerCustomCmds + with TableInfo<$ServerCustomCmdsTable, ServerCustomCmdRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ServerCustomCmdsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _cmdMeta = const VerificationMeta('cmd'); + @override + late final GeneratedColumn cmd = GeneratedColumn( + 'cmd', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [serverId, name, cmd]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'server_custom_cmd'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('cmd')) { + context.handle( + _cmdMeta, + cmd.isAcceptableOrUnknown(data['cmd']!, _cmdMeta), + ); + } else if (isInserting) { + context.missing(_cmdMeta); + } + return context; + } + + @override + Set get $primaryKey => {serverId, name}; + @override + ServerCustomCmdRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ServerCustomCmdRow( + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + cmd: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cmd'], + )!, + ); + } + + @override + $ServerCustomCmdsTable createAlias(String alias) { + return $ServerCustomCmdsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class ServerCustomCmdRow extends DataClass + implements Insertable { + final String serverId; + final String name; + final String cmd; + const ServerCustomCmdRow({ + required this.serverId, + required this.name, + required this.cmd, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['server_id'] = Variable(serverId); + map['name'] = Variable(name); + map['cmd'] = Variable(cmd); + return map; + } + + ServerCustomCmdsCompanion toCompanion(bool nullToAbsent) { + return ServerCustomCmdsCompanion( + serverId: Value(serverId), + name: Value(name), + cmd: Value(cmd), + ); + } + + factory ServerCustomCmdRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ServerCustomCmdRow( + serverId: serializer.fromJson(json['serverId']), + name: serializer.fromJson(json['name']), + cmd: serializer.fromJson(json['cmd']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'serverId': serializer.toJson(serverId), + 'name': serializer.toJson(name), + 'cmd': serializer.toJson(cmd), + }; + } + + ServerCustomCmdRow copyWith({String? serverId, String? name, String? cmd}) => + ServerCustomCmdRow( + serverId: serverId ?? this.serverId, + name: name ?? this.name, + cmd: cmd ?? this.cmd, + ); + ServerCustomCmdRow copyWithCompanion(ServerCustomCmdsCompanion data) { + return ServerCustomCmdRow( + serverId: data.serverId.present ? data.serverId.value : this.serverId, + name: data.name.present ? data.name.value : this.name, + cmd: data.cmd.present ? data.cmd.value : this.cmd, + ); + } + + @override + String toString() { + return (StringBuffer('ServerCustomCmdRow(') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('cmd: $cmd') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(serverId, name, cmd); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ServerCustomCmdRow && + other.serverId == this.serverId && + other.name == this.name && + other.cmd == this.cmd); +} + +class ServerCustomCmdsCompanion extends UpdateCompanion { + final Value serverId; + final Value name; + final Value cmd; + const ServerCustomCmdsCompanion({ + this.serverId = const Value.absent(), + this.name = const Value.absent(), + this.cmd = const Value.absent(), + }); + ServerCustomCmdsCompanion.insert({ + required String serverId, + required String name, + required String cmd, + }) : serverId = Value(serverId), + name = Value(name), + cmd = Value(cmd); + static Insertable custom({ + Expression? serverId, + Expression? name, + Expression? cmd, + }) { + return RawValuesInsertable({ + if (serverId != null) 'server_id': serverId, + if (name != null) 'name': name, + if (cmd != null) 'cmd': cmd, + }); + } + + ServerCustomCmdsCompanion copyWith({ + Value? serverId, + Value? name, + Value? cmd, + }) { + return ServerCustomCmdsCompanion( + serverId: serverId ?? this.serverId, + name: name ?? this.name, + cmd: cmd ?? this.cmd, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (cmd.present) { + map['cmd'] = Variable(cmd.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ServerCustomCmdsCompanion(') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('cmd: $cmd') + ..write(')')) + .toString(); + } +} + +class $KnownHostsTable extends KnownHosts + with TableInfo<$KnownHostsTable, KnownHostRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $KnownHostsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _keyTypeMeta = const VerificationMeta( + 'keyType', + ); + @override + late final GeneratedColumn keyType = GeneratedColumn( + 'key_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _fingerprintMeta = const VerificationMeta( + 'fingerprint', + ); + @override + late final GeneratedColumn fingerprint = GeneratedColumn( + 'fingerprint', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [serverId, keyType, fingerprint]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'known_host'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('key_type')) { + context.handle( + _keyTypeMeta, + keyType.isAcceptableOrUnknown(data['key_type']!, _keyTypeMeta), + ); + } else if (isInserting) { + context.missing(_keyTypeMeta); + } + if (data.containsKey('fingerprint')) { + context.handle( + _fingerprintMeta, + fingerprint.isAcceptableOrUnknown( + data['fingerprint']!, + _fingerprintMeta, + ), + ); + } else if (isInserting) { + context.missing(_fingerprintMeta); + } + return context; + } + + @override + Set get $primaryKey => {serverId, keyType}; + @override + KnownHostRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return KnownHostRow( + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + keyType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key_type'], + )!, + fingerprint: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}fingerprint'], + )!, + ); + } + + @override + $KnownHostsTable createAlias(String alias) { + return $KnownHostsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class KnownHostRow extends DataClass implements Insertable { + final String serverId; + final String keyType; + final String fingerprint; + const KnownHostRow({ + required this.serverId, + required this.keyType, + required this.fingerprint, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['server_id'] = Variable(serverId); + map['key_type'] = Variable(keyType); + map['fingerprint'] = Variable(fingerprint); + return map; + } + + KnownHostsCompanion toCompanion(bool nullToAbsent) { + return KnownHostsCompanion( + serverId: Value(serverId), + keyType: Value(keyType), + fingerprint: Value(fingerprint), + ); + } + + factory KnownHostRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return KnownHostRow( + serverId: serializer.fromJson(json['serverId']), + keyType: serializer.fromJson(json['keyType']), + fingerprint: serializer.fromJson(json['fingerprint']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'serverId': serializer.toJson(serverId), + 'keyType': serializer.toJson(keyType), + 'fingerprint': serializer.toJson(fingerprint), + }; + } + + KnownHostRow copyWith({ + String? serverId, + String? keyType, + String? fingerprint, + }) => KnownHostRow( + serverId: serverId ?? this.serverId, + keyType: keyType ?? this.keyType, + fingerprint: fingerprint ?? this.fingerprint, + ); + KnownHostRow copyWithCompanion(KnownHostsCompanion data) { + return KnownHostRow( + serverId: data.serverId.present ? data.serverId.value : this.serverId, + keyType: data.keyType.present ? data.keyType.value : this.keyType, + fingerprint: data.fingerprint.present + ? data.fingerprint.value + : this.fingerprint, + ); + } + + @override + String toString() { + return (StringBuffer('KnownHostRow(') + ..write('serverId: $serverId, ') + ..write('keyType: $keyType, ') + ..write('fingerprint: $fingerprint') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(serverId, keyType, fingerprint); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is KnownHostRow && + other.serverId == this.serverId && + other.keyType == this.keyType && + other.fingerprint == this.fingerprint); +} + +class KnownHostsCompanion extends UpdateCompanion { + final Value serverId; + final Value keyType; + final Value fingerprint; + const KnownHostsCompanion({ + this.serverId = const Value.absent(), + this.keyType = const Value.absent(), + this.fingerprint = const Value.absent(), + }); + KnownHostsCompanion.insert({ + required String serverId, + required String keyType, + required String fingerprint, + }) : serverId = Value(serverId), + keyType = Value(keyType), + fingerprint = Value(fingerprint); + static Insertable custom({ + Expression? serverId, + Expression? keyType, + Expression? fingerprint, + }) { + return RawValuesInsertable({ + if (serverId != null) 'server_id': serverId, + if (keyType != null) 'key_type': keyType, + if (fingerprint != null) 'fingerprint': fingerprint, + }); + } + + KnownHostsCompanion copyWith({ + Value? serverId, + Value? keyType, + Value? fingerprint, + }) { + return KnownHostsCompanion( + serverId: serverId ?? this.serverId, + keyType: keyType ?? this.keyType, + fingerprint: fingerprint ?? this.fingerprint, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (keyType.present) { + map['key_type'] = Variable(keyType.value); + } + if (fingerprint.present) { + map['fingerprint'] = Variable(fingerprint.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('KnownHostsCompanion(') + ..write('serverId: $serverId, ') + ..write('keyType: $keyType, ') + ..write('fingerprint: $fingerprint') + ..write(')')) + .toString(); + } +} + +class $SnippetsTable extends Snippets + with TableInfo<$SnippetsTable, SnippetRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SnippetsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _revMeta = const VerificationMeta('rev'); + @override + late final GeneratedColumn rev = GeneratedColumn( + 'rev', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _scriptMeta = const VerificationMeta('script'); + @override + late final GeneratedColumn script = GeneratedColumn( + 'script', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _noteMeta = const VerificationMeta('note'); + @override + late final GeneratedColumn note = GeneratedColumn( + 'note', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + updatedAt, + rev, + id, + name, + script, + note, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'snippet'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('rev')) { + context.handle( + _revMeta, + rev.isAcceptableOrUnknown(data['rev']!, _revMeta), + ); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('script')) { + context.handle( + _scriptMeta, + script.isAcceptableOrUnknown(data['script']!, _scriptMeta), + ); + } else if (isInserting) { + context.missing(_scriptMeta); + } + if (data.containsKey('note')) { + context.handle( + _noteMeta, + note.isAcceptableOrUnknown(data['note']!, _noteMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + SnippetRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SnippetRow( + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + rev: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rev'], + )!, + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + script: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}script'], + )!, + note: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}note'], + ), + ); + } + + @override + $SnippetsTable createAlias(String alias) { + return $SnippetsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class SnippetRow extends DataClass implements Insertable { + final int updatedAt; + final int rev; + final String id; + final String name; + final String script; + final String? note; + const SnippetRow({ + required this.updatedAt, + required this.rev, + required this.id, + required this.name, + required this.script, + this.note, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['updated_at'] = Variable(updatedAt); + map['rev'] = Variable(rev); + map['id'] = Variable(id); + map['name'] = Variable(name); + map['script'] = Variable(script); + if (!nullToAbsent || note != null) { + map['note'] = Variable(note); + } + return map; + } + + SnippetsCompanion toCompanion(bool nullToAbsent) { + return SnippetsCompanion( + updatedAt: Value(updatedAt), + rev: Value(rev), + id: Value(id), + name: Value(name), + script: Value(script), + note: note == null && nullToAbsent ? const Value.absent() : Value(note), + ); + } + + factory SnippetRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SnippetRow( + updatedAt: serializer.fromJson(json['updatedAt']), + rev: serializer.fromJson(json['rev']), + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + script: serializer.fromJson(json['script']), + note: serializer.fromJson(json['note']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'updatedAt': serializer.toJson(updatedAt), + 'rev': serializer.toJson(rev), + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'script': serializer.toJson(script), + 'note': serializer.toJson(note), + }; + } + + SnippetRow copyWith({ + int? updatedAt, + int? rev, + String? id, + String? name, + String? script, + Value note = const Value.absent(), + }) => SnippetRow( + updatedAt: updatedAt ?? this.updatedAt, + rev: rev ?? this.rev, + id: id ?? this.id, + name: name ?? this.name, + script: script ?? this.script, + note: note.present ? note.value : this.note, + ); + SnippetRow copyWithCompanion(SnippetsCompanion data) { + return SnippetRow( + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + rev: data.rev.present ? data.rev.value : this.rev, + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + script: data.script.present ? data.script.value : this.script, + note: data.note.present ? data.note.value : this.note, + ); + } + + @override + String toString() { + return (StringBuffer('SnippetRow(') + ..write('updatedAt: $updatedAt, ') + ..write('rev: $rev, ') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('script: $script, ') + ..write('note: $note') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(updatedAt, rev, id, name, script, note); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SnippetRow && + other.updatedAt == this.updatedAt && + other.rev == this.rev && + other.id == this.id && + other.name == this.name && + other.script == this.script && + other.note == this.note); +} + +class SnippetsCompanion extends UpdateCompanion { + final Value updatedAt; + final Value rev; + final Value id; + final Value name; + final Value script; + final Value note; + const SnippetsCompanion({ + this.updatedAt = const Value.absent(), + this.rev = const Value.absent(), + this.id = const Value.absent(), + this.name = const Value.absent(), + this.script = const Value.absent(), + this.note = const Value.absent(), + }); + SnippetsCompanion.insert({ + this.updatedAt = const Value.absent(), + this.rev = const Value.absent(), + required String id, + required String name, + required String script, + this.note = const Value.absent(), + }) : id = Value(id), + name = Value(name), + script = Value(script); + static Insertable custom({ + Expression? updatedAt, + Expression? rev, + Expression? id, + Expression? name, + Expression? script, + Expression? note, + }) { + return RawValuesInsertable({ + if (updatedAt != null) 'updated_at': updatedAt, + if (rev != null) 'rev': rev, + if (id != null) 'id': id, + if (name != null) 'name': name, + if (script != null) 'script': script, + if (note != null) 'note': note, + }); + } + + SnippetsCompanion copyWith({ + Value? updatedAt, + Value? rev, + Value? id, + Value? name, + Value? script, + Value? note, + }) { + return SnippetsCompanion( + updatedAt: updatedAt ?? this.updatedAt, + rev: rev ?? this.rev, + id: id ?? this.id, + name: name ?? this.name, + script: script ?? this.script, + note: note ?? this.note, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (script.present) { + map['script'] = Variable(script.value); + } + if (note.present) { + map['note'] = Variable(note.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SnippetsCompanion(') + ..write('updatedAt: $updatedAt, ') + ..write('rev: $rev, ') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('script: $script, ') + ..write('note: $note') + ..write(')')) + .toString(); + } +} + +class $SnippetTagsTable extends SnippetTags + with TableInfo<$SnippetTagsTable, SnippetTagRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SnippetTagsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _snippetIdMeta = const VerificationMeta( + 'snippetId', + ); + @override + late final GeneratedColumn snippetId = GeneratedColumn( + 'snippet_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES snippet (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _tagMeta = const VerificationMeta('tag'); + @override + late final GeneratedColumn tag = GeneratedColumn( + 'tag', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [snippetId, tag]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'snippet_tag'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('snippet_id')) { + context.handle( + _snippetIdMeta, + snippetId.isAcceptableOrUnknown(data['snippet_id']!, _snippetIdMeta), + ); + } else if (isInserting) { + context.missing(_snippetIdMeta); + } + if (data.containsKey('tag')) { + context.handle( + _tagMeta, + tag.isAcceptableOrUnknown(data['tag']!, _tagMeta), + ); + } else if (isInserting) { + context.missing(_tagMeta); + } + return context; + } + + @override + Set get $primaryKey => {snippetId, tag}; + @override + SnippetTagRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SnippetTagRow( + snippetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}snippet_id'], + )!, + tag: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}tag'], + )!, + ); + } + + @override + $SnippetTagsTable createAlias(String alias) { + return $SnippetTagsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class SnippetTagRow extends DataClass implements Insertable { + final String snippetId; + final String tag; + const SnippetTagRow({required this.snippetId, required this.tag}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['snippet_id'] = Variable(snippetId); + map['tag'] = Variable(tag); + return map; + } + + SnippetTagsCompanion toCompanion(bool nullToAbsent) { + return SnippetTagsCompanion(snippetId: Value(snippetId), tag: Value(tag)); + } + + factory SnippetTagRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SnippetTagRow( + snippetId: serializer.fromJson(json['snippetId']), + tag: serializer.fromJson(json['tag']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'snippetId': serializer.toJson(snippetId), + 'tag': serializer.toJson(tag), + }; + } + + SnippetTagRow copyWith({String? snippetId, String? tag}) => SnippetTagRow( + snippetId: snippetId ?? this.snippetId, + tag: tag ?? this.tag, + ); + SnippetTagRow copyWithCompanion(SnippetTagsCompanion data) { + return SnippetTagRow( + snippetId: data.snippetId.present ? data.snippetId.value : this.snippetId, + tag: data.tag.present ? data.tag.value : this.tag, + ); + } + + @override + String toString() { + return (StringBuffer('SnippetTagRow(') + ..write('snippetId: $snippetId, ') + ..write('tag: $tag') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(snippetId, tag); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SnippetTagRow && + other.snippetId == this.snippetId && + other.tag == this.tag); +} + +class SnippetTagsCompanion extends UpdateCompanion { + final Value snippetId; + final Value tag; + const SnippetTagsCompanion({ + this.snippetId = const Value.absent(), + this.tag = const Value.absent(), + }); + SnippetTagsCompanion.insert({required String snippetId, required String tag}) + : snippetId = Value(snippetId), + tag = Value(tag); + static Insertable custom({ + Expression? snippetId, + Expression? tag, + }) { + return RawValuesInsertable({ + if (snippetId != null) 'snippet_id': snippetId, + if (tag != null) 'tag': tag, + }); + } + + SnippetTagsCompanion copyWith({ + Value? snippetId, + Value? tag, + }) { + return SnippetTagsCompanion( + snippetId: snippetId ?? this.snippetId, + tag: tag ?? this.tag, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (snippetId.present) { + map['snippet_id'] = Variable(snippetId.value); + } + if (tag.present) { + map['tag'] = Variable(tag.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SnippetTagsCompanion(') + ..write('snippetId: $snippetId, ') + ..write('tag: $tag') + ..write(')')) + .toString(); + } +} + +class $SnippetAutoRunOnTable extends SnippetAutoRunOn + with TableInfo<$SnippetAutoRunOnTable, SnippetAutoRunRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SnippetAutoRunOnTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _snippetIdMeta = const VerificationMeta( + 'snippetId', + ); + @override + late final GeneratedColumn snippetId = GeneratedColumn( + 'snippet_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES snippet (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [snippetId, serverId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'snippet_auto_run_on'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('snippet_id')) { + context.handle( + _snippetIdMeta, + snippetId.isAcceptableOrUnknown(data['snippet_id']!, _snippetIdMeta), + ); + } else if (isInserting) { + context.missing(_snippetIdMeta); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {snippetId, serverId}; + @override + SnippetAutoRunRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SnippetAutoRunRow( + snippetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}snippet_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + ); + } + + @override + $SnippetAutoRunOnTable createAlias(String alias) { + return $SnippetAutoRunOnTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class SnippetAutoRunRow extends DataClass + implements Insertable { + final String snippetId; + final String serverId; + const SnippetAutoRunRow({required this.snippetId, required this.serverId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['snippet_id'] = Variable(snippetId); + map['server_id'] = Variable(serverId); + return map; + } + + SnippetAutoRunOnCompanion toCompanion(bool nullToAbsent) { + return SnippetAutoRunOnCompanion( + snippetId: Value(snippetId), + serverId: Value(serverId), + ); + } + + factory SnippetAutoRunRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SnippetAutoRunRow( + snippetId: serializer.fromJson(json['snippetId']), + serverId: serializer.fromJson(json['serverId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'snippetId': serializer.toJson(snippetId), + 'serverId': serializer.toJson(serverId), + }; + } + + SnippetAutoRunRow copyWith({String? snippetId, String? serverId}) => + SnippetAutoRunRow( + snippetId: snippetId ?? this.snippetId, + serverId: serverId ?? this.serverId, + ); + SnippetAutoRunRow copyWithCompanion(SnippetAutoRunOnCompanion data) { + return SnippetAutoRunRow( + snippetId: data.snippetId.present ? data.snippetId.value : this.snippetId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + ); + } + + @override + String toString() { + return (StringBuffer('SnippetAutoRunRow(') + ..write('snippetId: $snippetId, ') + ..write('serverId: $serverId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(snippetId, serverId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SnippetAutoRunRow && + other.snippetId == this.snippetId && + other.serverId == this.serverId); +} + +class SnippetAutoRunOnCompanion extends UpdateCompanion { + final Value snippetId; + final Value serverId; + const SnippetAutoRunOnCompanion({ + this.snippetId = const Value.absent(), + this.serverId = const Value.absent(), + }); + SnippetAutoRunOnCompanion.insert({ + required String snippetId, + required String serverId, + }) : snippetId = Value(snippetId), + serverId = Value(serverId); + static Insertable custom({ + Expression? snippetId, + Expression? serverId, + }) { + return RawValuesInsertable({ + if (snippetId != null) 'snippet_id': snippetId, + if (serverId != null) 'server_id': serverId, + }); + } + + SnippetAutoRunOnCompanion copyWith({ + Value? snippetId, + Value? serverId, + }) { + return SnippetAutoRunOnCompanion( + snippetId: snippetId ?? this.snippetId, + serverId: serverId ?? this.serverId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (snippetId.present) { + map['snippet_id'] = Variable(snippetId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SnippetAutoRunOnCompanion(') + ..write('snippetId: $snippetId, ') + ..write('serverId: $serverId') + ..write(')')) + .toString(); + } +} + +class $PortForwardsTable extends PortForwards + with TableInfo<$PortForwardsTable, PortForwardRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PortForwardsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _revMeta = const VerificationMeta('rev'); + @override + late final GeneratedColumn rev = GeneratedColumn( + 'rev', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _localHostMeta = const VerificationMeta( + 'localHost', + ); + @override + late final GeneratedColumn localHost = GeneratedColumn( + 'local_host', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _localPortMeta = const VerificationMeta( + 'localPort', + ); + @override + late final GeneratedColumn localPort = GeneratedColumn( + 'local_port', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _remoteHostMeta = const VerificationMeta( + 'remoteHost', + ); + @override + late final GeneratedColumn remoteHost = GeneratedColumn( + 'remote_host', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _remotePortMeta = const VerificationMeta( + 'remotePort', + ); + @override + late final GeneratedColumn remotePort = GeneratedColumn( + 'remote_port', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + updatedAt, + rev, + id, + serverId, + name, + type, + localHost, + localPort, + remoteHost, + remotePort, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'port_forward'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('rev')) { + context.handle( + _revMeta, + rev.isAcceptableOrUnknown(data['rev']!, _revMeta), + ); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), + ); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('local_host')) { + context.handle( + _localHostMeta, + localHost.isAcceptableOrUnknown(data['local_host']!, _localHostMeta), + ); + } + if (data.containsKey('local_port')) { + context.handle( + _localPortMeta, + localPort.isAcceptableOrUnknown(data['local_port']!, _localPortMeta), + ); + } + if (data.containsKey('remote_host')) { + context.handle( + _remoteHostMeta, + remoteHost.isAcceptableOrUnknown(data['remote_host']!, _remoteHostMeta), + ); + } + if (data.containsKey('remote_port')) { + context.handle( + _remotePortMeta, + remotePort.isAcceptableOrUnknown(data['remote_port']!, _remotePortMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PortForwardRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PortForwardRow( + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + rev: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rev'], + )!, + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + localHost: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_host'], + ), + localPort: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}local_port'], + )!, + remoteHost: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}remote_host'], + ), + remotePort: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}remote_port'], + ), + ); + } + + @override + $PortForwardsTable createAlias(String alias) { + return $PortForwardsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class PortForwardRow extends DataClass implements Insertable { + final int updatedAt; + final int rev; + final String id; + final String serverId; + final String name; + final String type; + final String? localHost; + final int localPort; + final String? remoteHost; + final int? remotePort; + const PortForwardRow({ + required this.updatedAt, + required this.rev, + required this.id, + required this.serverId, + required this.name, + required this.type, + this.localHost, + required this.localPort, + this.remoteHost, + this.remotePort, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['updated_at'] = Variable(updatedAt); + map['rev'] = Variable(rev); + map['id'] = Variable(id); + map['server_id'] = Variable(serverId); + map['name'] = Variable(name); + map['type'] = Variable(type); + if (!nullToAbsent || localHost != null) { + map['local_host'] = Variable(localHost); + } + map['local_port'] = Variable(localPort); + if (!nullToAbsent || remoteHost != null) { + map['remote_host'] = Variable(remoteHost); + } + if (!nullToAbsent || remotePort != null) { + map['remote_port'] = Variable(remotePort); + } + return map; + } + + PortForwardsCompanion toCompanion(bool nullToAbsent) { + return PortForwardsCompanion( + updatedAt: Value(updatedAt), + rev: Value(rev), + id: Value(id), + serverId: Value(serverId), + name: Value(name), + type: Value(type), + localHost: localHost == null && nullToAbsent + ? const Value.absent() + : Value(localHost), + localPort: Value(localPort), + remoteHost: remoteHost == null && nullToAbsent + ? const Value.absent() + : Value(remoteHost), + remotePort: remotePort == null && nullToAbsent + ? const Value.absent() + : Value(remotePort), + ); + } + + factory PortForwardRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PortForwardRow( + updatedAt: serializer.fromJson(json['updatedAt']), + rev: serializer.fromJson(json['rev']), + id: serializer.fromJson(json['id']), + serverId: serializer.fromJson(json['serverId']), + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + localHost: serializer.fromJson(json['localHost']), + localPort: serializer.fromJson(json['localPort']), + remoteHost: serializer.fromJson(json['remoteHost']), + remotePort: serializer.fromJson(json['remotePort']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'updatedAt': serializer.toJson(updatedAt), + 'rev': serializer.toJson(rev), + 'id': serializer.toJson(id), + 'serverId': serializer.toJson(serverId), + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'localHost': serializer.toJson(localHost), + 'localPort': serializer.toJson(localPort), + 'remoteHost': serializer.toJson(remoteHost), + 'remotePort': serializer.toJson(remotePort), + }; + } + + PortForwardRow copyWith({ + int? updatedAt, + int? rev, + String? id, + String? serverId, + String? name, + String? type, + Value localHost = const Value.absent(), + int? localPort, + Value remoteHost = const Value.absent(), + Value remotePort = const Value.absent(), + }) => PortForwardRow( + updatedAt: updatedAt ?? this.updatedAt, + rev: rev ?? this.rev, + id: id ?? this.id, + serverId: serverId ?? this.serverId, + name: name ?? this.name, + type: type ?? this.type, + localHost: localHost.present ? localHost.value : this.localHost, + localPort: localPort ?? this.localPort, + remoteHost: remoteHost.present ? remoteHost.value : this.remoteHost, + remotePort: remotePort.present ? remotePort.value : this.remotePort, + ); + PortForwardRow copyWithCompanion(PortForwardsCompanion data) { + return PortForwardRow( + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + rev: data.rev.present ? data.rev.value : this.rev, + id: data.id.present ? data.id.value : this.id, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + localHost: data.localHost.present ? data.localHost.value : this.localHost, + localPort: data.localPort.present ? data.localPort.value : this.localPort, + remoteHost: data.remoteHost.present + ? data.remoteHost.value + : this.remoteHost, + remotePort: data.remotePort.present + ? data.remotePort.value + : this.remotePort, + ); + } + + @override + String toString() { + return (StringBuffer('PortForwardRow(') + ..write('updatedAt: $updatedAt, ') + ..write('rev: $rev, ') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('localHost: $localHost, ') + ..write('localPort: $localPort, ') + ..write('remoteHost: $remoteHost, ') + ..write('remotePort: $remotePort') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + updatedAt, + rev, + id, + serverId, + name, + type, + localHost, + localPort, + remoteHost, + remotePort, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PortForwardRow && + other.updatedAt == this.updatedAt && + other.rev == this.rev && + other.id == this.id && + other.serverId == this.serverId && + other.name == this.name && + other.type == this.type && + other.localHost == this.localHost && + other.localPort == this.localPort && + other.remoteHost == this.remoteHost && + other.remotePort == this.remotePort); +} + +class PortForwardsCompanion extends UpdateCompanion { + final Value updatedAt; + final Value rev; + final Value id; + final Value serverId; + final Value name; + final Value type; + final Value localHost; + final Value localPort; + final Value remoteHost; + final Value remotePort; + const PortForwardsCompanion({ + this.updatedAt = const Value.absent(), + this.rev = const Value.absent(), + this.id = const Value.absent(), + this.serverId = const Value.absent(), + this.name = const Value.absent(), + this.type = const Value.absent(), + this.localHost = const Value.absent(), + this.localPort = const Value.absent(), + this.remoteHost = const Value.absent(), + this.remotePort = const Value.absent(), + }); + PortForwardsCompanion.insert({ + this.updatedAt = const Value.absent(), + this.rev = const Value.absent(), + required String id, + required String serverId, + required String name, + required String type, + this.localHost = const Value.absent(), + this.localPort = const Value.absent(), + this.remoteHost = const Value.absent(), + this.remotePort = const Value.absent(), + }) : id = Value(id), + serverId = Value(serverId), + name = Value(name), + type = Value(type); + static Insertable custom({ + Expression? updatedAt, + Expression? rev, + Expression? id, + Expression? serverId, + Expression? name, + Expression? type, + Expression? localHost, + Expression? localPort, + Expression? remoteHost, + Expression? remotePort, + }) { + return RawValuesInsertable({ + if (updatedAt != null) 'updated_at': updatedAt, + if (rev != null) 'rev': rev, + if (id != null) 'id': id, + if (serverId != null) 'server_id': serverId, + if (name != null) 'name': name, + if (type != null) 'type': type, + if (localHost != null) 'local_host': localHost, + if (localPort != null) 'local_port': localPort, + if (remoteHost != null) 'remote_host': remoteHost, + if (remotePort != null) 'remote_port': remotePort, + }); + } + + PortForwardsCompanion copyWith({ + Value? updatedAt, + Value? rev, + Value? id, + Value? serverId, + Value? name, + Value? type, + Value? localHost, + Value? localPort, + Value? remoteHost, + Value? remotePort, + }) { + return PortForwardsCompanion( + updatedAt: updatedAt ?? this.updatedAt, + rev: rev ?? this.rev, + id: id ?? this.id, + serverId: serverId ?? this.serverId, + name: name ?? this.name, + type: type ?? this.type, + localHost: localHost ?? this.localHost, + localPort: localPort ?? this.localPort, + remoteHost: remoteHost ?? this.remoteHost, + remotePort: remotePort ?? this.remotePort, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (localHost.present) { + map['local_host'] = Variable(localHost.value); + } + if (localPort.present) { + map['local_port'] = Variable(localPort.value); + } + if (remoteHost.present) { + map['remote_host'] = Variable(remoteHost.value); + } + if (remotePort.present) { + map['remote_port'] = Variable(remotePort.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PortForwardsCompanion(') + ..write('updatedAt: $updatedAt, ') + ..write('rev: $rev, ') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('localHost: $localHost, ') + ..write('localPort: $localPort, ') + ..write('remoteHost: $remoteHost, ') + ..write('remotePort: $remotePort') + ..write(')')) + .toString(); + } +} + +class $ContainerHostsTable extends ContainerHosts + with TableInfo<$ContainerHostsTable, ContainerHostRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ContainerHostsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _hostMeta = const VerificationMeta('host'); + @override + late final GeneratedColumn host = GeneratedColumn( + 'host', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [serverId, type, host]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'container_host'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), + ); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('host')) { + context.handle( + _hostMeta, + host.isAcceptableOrUnknown(data['host']!, _hostMeta), + ); + } else if (isInserting) { + context.missing(_hostMeta); + } + return context; + } + + @override + Set get $primaryKey => {serverId, type}; + @override + ContainerHostRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ContainerHostRow( + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + host: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}host'], + )!, + ); + } + + @override + $ContainerHostsTable createAlias(String alias) { + return $ContainerHostsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class ContainerHostRow extends DataClass + implements Insertable { + final String serverId; + final String type; + final String host; + const ContainerHostRow({ + required this.serverId, + required this.type, + required this.host, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['server_id'] = Variable(serverId); + map['type'] = Variable(type); + map['host'] = Variable(host); + return map; + } + + ContainerHostsCompanion toCompanion(bool nullToAbsent) { + return ContainerHostsCompanion( + serverId: Value(serverId), + type: Value(type), + host: Value(host), + ); + } + + factory ContainerHostRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ContainerHostRow( + serverId: serializer.fromJson(json['serverId']), + type: serializer.fromJson(json['type']), + host: serializer.fromJson(json['host']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'serverId': serializer.toJson(serverId), + 'type': serializer.toJson(type), + 'host': serializer.toJson(host), + }; + } + + ContainerHostRow copyWith({String? serverId, String? type, String? host}) => + ContainerHostRow( + serverId: serverId ?? this.serverId, + type: type ?? this.type, + host: host ?? this.host, + ); + ContainerHostRow copyWithCompanion(ContainerHostsCompanion data) { + return ContainerHostRow( + serverId: data.serverId.present ? data.serverId.value : this.serverId, + type: data.type.present ? data.type.value : this.type, + host: data.host.present ? data.host.value : this.host, + ); + } + + @override + String toString() { + return (StringBuffer('ContainerHostRow(') + ..write('serverId: $serverId, ') + ..write('type: $type, ') + ..write('host: $host') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(serverId, type, host); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ContainerHostRow && + other.serverId == this.serverId && + other.type == this.type && + other.host == this.host); +} + +class ContainerHostsCompanion extends UpdateCompanion { + final Value serverId; + final Value type; + final Value host; + const ContainerHostsCompanion({ + this.serverId = const Value.absent(), + this.type = const Value.absent(), + this.host = const Value.absent(), + }); + ContainerHostsCompanion.insert({ + required String serverId, + required String type, + required String host, + }) : serverId = Value(serverId), + type = Value(type), + host = Value(host); + static Insertable custom({ + Expression? serverId, + Expression? type, + Expression? host, + }) { + return RawValuesInsertable({ + if (serverId != null) 'server_id': serverId, + if (type != null) 'type': type, + if (host != null) 'host': host, + }); + } + + ContainerHostsCompanion copyWith({ + Value? serverId, + Value? type, + Value? host, + }) { + return ContainerHostsCompanion( + serverId: serverId ?? this.serverId, + type: type ?? this.type, + host: host ?? this.host, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (host.present) { + map['host'] = Variable(host.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContainerHostsCompanion(') + ..write('serverId: $serverId, ') + ..write('type: $type, ') + ..write('host: $host') + ..write(')')) + .toString(); + } +} + +class $ContainerRuntimesTable extends ContainerRuntimes + with TableInfo<$ContainerRuntimesTable, ContainerRuntimeRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ContainerRuntimesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [serverId, type]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'container_runtime'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), + ); + } else if (isInserting) { + context.missing(_typeMeta); + } + return context; + } + + @override + Set get $primaryKey => {serverId}; + @override + ContainerRuntimeRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ContainerRuntimeRow( + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + ); + } + + @override + $ContainerRuntimesTable createAlias(String alias) { + return $ContainerRuntimesTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class ContainerRuntimeRow extends DataClass + implements Insertable { + final String serverId; + final String type; + const ContainerRuntimeRow({required this.serverId, required this.type}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['server_id'] = Variable(serverId); + map['type'] = Variable(type); + return map; + } + + ContainerRuntimesCompanion toCompanion(bool nullToAbsent) { + return ContainerRuntimesCompanion( + serverId: Value(serverId), + type: Value(type), + ); + } + + factory ContainerRuntimeRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ContainerRuntimeRow( + serverId: serializer.fromJson(json['serverId']), + type: serializer.fromJson(json['type']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'serverId': serializer.toJson(serverId), + 'type': serializer.toJson(type), + }; + } + + ContainerRuntimeRow copyWith({String? serverId, String? type}) => + ContainerRuntimeRow( + serverId: serverId ?? this.serverId, + type: type ?? this.type, + ); + ContainerRuntimeRow copyWithCompanion(ContainerRuntimesCompanion data) { + return ContainerRuntimeRow( + serverId: data.serverId.present ? data.serverId.value : this.serverId, + type: data.type.present ? data.type.value : this.type, + ); + } + + @override + String toString() { + return (StringBuffer('ContainerRuntimeRow(') + ..write('serverId: $serverId, ') + ..write('type: $type') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(serverId, type); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ContainerRuntimeRow && + other.serverId == this.serverId && + other.type == this.type); +} + +class ContainerRuntimesCompanion extends UpdateCompanion { + final Value serverId; + final Value type; + const ContainerRuntimesCompanion({ + this.serverId = const Value.absent(), + this.type = const Value.absent(), + }); + ContainerRuntimesCompanion.insert({ + required String serverId, + required String type, + }) : serverId = Value(serverId), + type = Value(type); + static Insertable custom({ + Expression? serverId, + Expression? type, + }) { + return RawValuesInsertable({ + if (serverId != null) 'server_id': serverId, + if (type != null) 'type': type, + }); + } + + ContainerRuntimesCompanion copyWith({ + Value? serverId, + Value? type, + }) { + return ContainerRuntimesCompanion( + serverId: serverId ?? this.serverId, + type: type ?? this.type, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContainerRuntimesCompanion(') + ..write('serverId: $serverId, ') + ..write('type: $type') + ..write(')')) + .toString(); + } +} + +class $ConnStatsTable extends ConnStats + with TableInfo<$ConnStatsTable, ConnStatRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ConnStatsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES server (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _serverNameMeta = const VerificationMeta( + 'serverName', + ); + @override + late final GeneratedColumn serverName = GeneratedColumn( + 'server_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _timestampMeta = const VerificationMeta( + 'timestamp', + ); + @override + late final GeneratedColumn timestamp = GeneratedColumn( + 'timestamp', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _resultMeta = const VerificationMeta('result'); + @override + late final GeneratedColumn result = GeneratedColumn( + 'result', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _errorMessageMeta = const VerificationMeta( + 'errorMessage', + ); + @override + late final GeneratedColumn errorMessage = GeneratedColumn( + 'error_message', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant(''), + ); + static const VerificationMeta _durationMsMeta = const VerificationMeta( + 'durationMs', + ); + @override + late final GeneratedColumn durationMs = GeneratedColumn( + 'duration_ms', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + serverId, + serverName, + timestamp, + result, + errorMessage, + durationMs, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'conn_stat'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('server_name')) { + context.handle( + _serverNameMeta, + serverName.isAcceptableOrUnknown(data['server_name']!, _serverNameMeta), + ); + } else if (isInserting) { + context.missing(_serverNameMeta); + } + if (data.containsKey('timestamp')) { + context.handle( + _timestampMeta, + timestamp.isAcceptableOrUnknown(data['timestamp']!, _timestampMeta), + ); + } else if (isInserting) { + context.missing(_timestampMeta); + } + if (data.containsKey('result')) { + context.handle( + _resultMeta, + result.isAcceptableOrUnknown(data['result']!, _resultMeta), + ); + } else if (isInserting) { + context.missing(_resultMeta); + } + if (data.containsKey('error_message')) { + context.handle( + _errorMessageMeta, + errorMessage.isAcceptableOrUnknown( + data['error_message']!, + _errorMessageMeta, + ), + ); + } + if (data.containsKey('duration_ms')) { + context.handle( + _durationMsMeta, + durationMs.isAcceptableOrUnknown(data['duration_ms']!, _durationMsMeta), + ); + } else if (isInserting) { + context.missing(_durationMsMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ConnStatRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ConnStatRow( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + serverName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_name'], + )!, + timestamp: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}timestamp'], + )!, + result: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}result'], + )!, + errorMessage: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}error_message'], + )!, + durationMs: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_ms'], + )!, + ); + } + + @override + $ConnStatsTable createAlias(String alias) { + return $ConnStatsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class ConnStatRow extends DataClass implements Insertable { + final String id; + final String serverId; + final String serverName; + final int timestamp; + final String result; + final String errorMessage; + final int durationMs; + const ConnStatRow({ + required this.id, + required this.serverId, + required this.serverName, + required this.timestamp, + required this.result, + required this.errorMessage, + required this.durationMs, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['server_id'] = Variable(serverId); + map['server_name'] = Variable(serverName); + map['timestamp'] = Variable(timestamp); + map['result'] = Variable(result); + map['error_message'] = Variable(errorMessage); + map['duration_ms'] = Variable(durationMs); + return map; + } + + ConnStatsCompanion toCompanion(bool nullToAbsent) { + return ConnStatsCompanion( + id: Value(id), + serverId: Value(serverId), + serverName: Value(serverName), + timestamp: Value(timestamp), + result: Value(result), + errorMessage: Value(errorMessage), + durationMs: Value(durationMs), + ); + } + + factory ConnStatRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ConnStatRow( + id: serializer.fromJson(json['id']), + serverId: serializer.fromJson(json['serverId']), + serverName: serializer.fromJson(json['serverName']), + timestamp: serializer.fromJson(json['timestamp']), + result: serializer.fromJson(json['result']), + errorMessage: serializer.fromJson(json['errorMessage']), + durationMs: serializer.fromJson(json['durationMs']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'serverId': serializer.toJson(serverId), + 'serverName': serializer.toJson(serverName), + 'timestamp': serializer.toJson(timestamp), + 'result': serializer.toJson(result), + 'errorMessage': serializer.toJson(errorMessage), + 'durationMs': serializer.toJson(durationMs), + }; + } + + ConnStatRow copyWith({ + String? id, + String? serverId, + String? serverName, + int? timestamp, + String? result, + String? errorMessage, + int? durationMs, + }) => ConnStatRow( + id: id ?? this.id, + serverId: serverId ?? this.serverId, + serverName: serverName ?? this.serverName, + timestamp: timestamp ?? this.timestamp, + result: result ?? this.result, + errorMessage: errorMessage ?? this.errorMessage, + durationMs: durationMs ?? this.durationMs, + ); + ConnStatRow copyWithCompanion(ConnStatsCompanion data) { + return ConnStatRow( + id: data.id.present ? data.id.value : this.id, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + serverName: data.serverName.present + ? data.serverName.value + : this.serverName, + timestamp: data.timestamp.present ? data.timestamp.value : this.timestamp, + result: data.result.present ? data.result.value : this.result, + errorMessage: data.errorMessage.present + ? data.errorMessage.value + : this.errorMessage, + durationMs: data.durationMs.present + ? data.durationMs.value + : this.durationMs, + ); + } + + @override + String toString() { + return (StringBuffer('ConnStatRow(') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('serverName: $serverName, ') + ..write('timestamp: $timestamp, ') + ..write('result: $result, ') + ..write('errorMessage: $errorMessage, ') + ..write('durationMs: $durationMs') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + serverId, + serverName, + timestamp, + result, + errorMessage, + durationMs, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ConnStatRow && + other.id == this.id && + other.serverId == this.serverId && + other.serverName == this.serverName && + other.timestamp == this.timestamp && + other.result == this.result && + other.errorMessage == this.errorMessage && + other.durationMs == this.durationMs); +} + +class ConnStatsCompanion extends UpdateCompanion { + final Value id; + final Value serverId; + final Value serverName; + final Value timestamp; + final Value result; + final Value errorMessage; + final Value durationMs; + const ConnStatsCompanion({ + this.id = const Value.absent(), + this.serverId = const Value.absent(), + this.serverName = const Value.absent(), + this.timestamp = const Value.absent(), + this.result = const Value.absent(), + this.errorMessage = const Value.absent(), + this.durationMs = const Value.absent(), + }); + ConnStatsCompanion.insert({ + required String id, + required String serverId, + required String serverName, + required int timestamp, + required String result, + this.errorMessage = const Value.absent(), + required int durationMs, + }) : id = Value(id), + serverId = Value(serverId), + serverName = Value(serverName), + timestamp = Value(timestamp), + result = Value(result), + durationMs = Value(durationMs); + static Insertable custom({ + Expression? id, + Expression? serverId, + Expression? serverName, + Expression? timestamp, + Expression? result, + Expression? errorMessage, + Expression? durationMs, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (serverId != null) 'server_id': serverId, + if (serverName != null) 'server_name': serverName, + if (timestamp != null) 'timestamp': timestamp, + if (result != null) 'result': result, + if (errorMessage != null) 'error_message': errorMessage, + if (durationMs != null) 'duration_ms': durationMs, + }); + } + + ConnStatsCompanion copyWith({ + Value? id, + Value? serverId, + Value? serverName, + Value? timestamp, + Value? result, + Value? errorMessage, + Value? durationMs, + }) { + return ConnStatsCompanion( + id: id ?? this.id, + serverId: serverId ?? this.serverId, + serverName: serverName ?? this.serverName, + timestamp: timestamp ?? this.timestamp, + result: result ?? this.result, + errorMessage: errorMessage ?? this.errorMessage, + durationMs: durationMs ?? this.durationMs, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (serverName.present) { + map['server_name'] = Variable(serverName.value); + } + if (timestamp.present) { + map['timestamp'] = Variable(timestamp.value); + } + if (result.present) { + map['result'] = Variable(result.value); + } + if (errorMessage.present) { + map['error_message'] = Variable(errorMessage.value); + } + if (durationMs.present) { + map['duration_ms'] = Variable(durationMs.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ConnStatsCompanion(') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('serverName: $serverName, ') + ..write('timestamp: $timestamp, ') + ..write('result: $result, ') + ..write('errorMessage: $errorMessage, ') + ..write('durationMs: $durationMs') + ..write(')')) + .toString(); + } +} + +class $AgentConversationsTable extends AgentConversations + with TableInfo<$AgentConversationsTable, AgentConversationRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $AgentConversationsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _dataMeta = const VerificationMeta('data'); + @override + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, serverId, updatedAt, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'agent_conversation'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } else if (isInserting) { + context.missing(_updatedAtMeta); + } + if (data.containsKey('data')) { + context.handle( + _dataMeta, + this.data.isAcceptableOrUnknown(data['data']!, _dataMeta), + ); + } else if (isInserting) { + context.missing(_dataMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + AgentConversationRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AgentConversationRow( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + ); + } + + @override + $AgentConversationsTable createAlias(String alias) { + return $AgentConversationsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class AgentConversationRow extends DataClass + implements Insertable { + final String id; + final String serverId; + final int updatedAt; + final String data; + const AgentConversationRow({ + required this.id, + required this.serverId, + required this.updatedAt, + required this.data, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['server_id'] = Variable(serverId); + map['updated_at'] = Variable(updatedAt); + map['data'] = Variable(data); + return map; + } + + AgentConversationsCompanion toCompanion(bool nullToAbsent) { + return AgentConversationsCompanion( + id: Value(id), + serverId: Value(serverId), + updatedAt: Value(updatedAt), + data: Value(data), + ); + } + + factory AgentConversationRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AgentConversationRow( + id: serializer.fromJson(json['id']), + serverId: serializer.fromJson(json['serverId']), + updatedAt: serializer.fromJson(json['updatedAt']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'serverId': serializer.toJson(serverId), + 'updatedAt': serializer.toJson(updatedAt), + 'data': serializer.toJson(data), + }; + } + + AgentConversationRow copyWith({ + String? id, + String? serverId, + int? updatedAt, + String? data, + }) => AgentConversationRow( + id: id ?? this.id, + serverId: serverId ?? this.serverId, + updatedAt: updatedAt ?? this.updatedAt, + data: data ?? this.data, + ); + AgentConversationRow copyWithCompanion(AgentConversationsCompanion data) { + return AgentConversationRow( + id: data.id.present ? data.id.value : this.id, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('AgentConversationRow(') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('updatedAt: $updatedAt, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, serverId, updatedAt, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AgentConversationRow && + other.id == this.id && + other.serverId == this.serverId && + other.updatedAt == this.updatedAt && + other.data == this.data); +} + +class AgentConversationsCompanion + extends UpdateCompanion { + final Value id; + final Value serverId; + final Value updatedAt; + final Value data; + const AgentConversationsCompanion({ + this.id = const Value.absent(), + this.serverId = const Value.absent(), + this.updatedAt = const Value.absent(), + this.data = const Value.absent(), + }); + AgentConversationsCompanion.insert({ + required String id, + required String serverId, + required int updatedAt, + required String data, + }) : id = Value(id), + serverId = Value(serverId), + updatedAt = Value(updatedAt), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? serverId, + Expression? updatedAt, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (serverId != null) 'server_id': serverId, + if (updatedAt != null) 'updated_at': updatedAt, + if (data != null) 'data': data, + }); + } + + AgentConversationsCompanion copyWith({ + Value? id, + Value? serverId, + Value? updatedAt, + Value? data, + }) { + return AgentConversationsCompanion( + id: id ?? this.id, + serverId: serverId ?? this.serverId, + updatedAt: updatedAt ?? this.updatedAt, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AgentConversationsCompanion(') + ..write('id: $id, ') + ..write('serverId: $serverId, ') + ..write('updatedAt: $updatedAt, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + +class $AgentActiveConversationsTable extends AgentActiveConversations + with TableInfo<$AgentActiveConversationsTable, AgentActiveConversationRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $AgentActiveConversationsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _conversationIdMeta = const VerificationMeta( + 'conversationId', + ); + @override + late final GeneratedColumn conversationId = GeneratedColumn( + 'conversation_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES agent_conversation (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [serverId, conversationId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'agent_active_conversation'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } else if (isInserting) { + context.missing(_serverIdMeta); + } + if (data.containsKey('conversation_id')) { + context.handle( + _conversationIdMeta, + conversationId.isAcceptableOrUnknown( + data['conversation_id']!, + _conversationIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_conversationIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {serverId}; + @override + AgentActiveConversationRow map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AgentActiveConversationRow( + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + conversationId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation_id'], + )!, + ); + } + + @override + $AgentActiveConversationsTable createAlias(String alias) { + return $AgentActiveConversationsTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class AgentActiveConversationRow extends DataClass + implements Insertable { + final String serverId; + final String conversationId; + const AgentActiveConversationRow({ + required this.serverId, + required this.conversationId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['server_id'] = Variable(serverId); + map['conversation_id'] = Variable(conversationId); + return map; + } + + AgentActiveConversationsCompanion toCompanion(bool nullToAbsent) { + return AgentActiveConversationsCompanion( + serverId: Value(serverId), + conversationId: Value(conversationId), + ); + } + + factory AgentActiveConversationRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AgentActiveConversationRow( + serverId: serializer.fromJson(json['serverId']), + conversationId: serializer.fromJson(json['conversationId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'serverId': serializer.toJson(serverId), + 'conversationId': serializer.toJson(conversationId), + }; + } + + AgentActiveConversationRow copyWith({ + String? serverId, + String? conversationId, + }) => AgentActiveConversationRow( + serverId: serverId ?? this.serverId, + conversationId: conversationId ?? this.conversationId, + ); + AgentActiveConversationRow copyWithCompanion( + AgentActiveConversationsCompanion data, + ) { + return AgentActiveConversationRow( + serverId: data.serverId.present ? data.serverId.value : this.serverId, + conversationId: data.conversationId.present + ? data.conversationId.value + : this.conversationId, + ); + } + + @override + String toString() { + return (StringBuffer('AgentActiveConversationRow(') + ..write('serverId: $serverId, ') + ..write('conversationId: $conversationId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(serverId, conversationId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AgentActiveConversationRow && + other.serverId == this.serverId && + other.conversationId == this.conversationId); +} + +class AgentActiveConversationsCompanion + extends UpdateCompanion { + final Value serverId; + final Value conversationId; + const AgentActiveConversationsCompanion({ + this.serverId = const Value.absent(), + this.conversationId = const Value.absent(), + }); + AgentActiveConversationsCompanion.insert({ + required String serverId, + required String conversationId, + }) : serverId = Value(serverId), + conversationId = Value(conversationId); + static Insertable custom({ + Expression? serverId, + Expression? conversationId, + }) { + return RawValuesInsertable({ + if (serverId != null) 'server_id': serverId, + if (conversationId != null) 'conversation_id': conversationId, + }); + } + + AgentActiveConversationsCompanion copyWith({ + Value? serverId, + Value? conversationId, + }) { + return AgentActiveConversationsCompanion( + serverId: serverId ?? this.serverId, + conversationId: conversationId ?? this.conversationId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (conversationId.present) { + map['conversation_id'] = Variable(conversationId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AgentActiveConversationsCompanion(') + ..write('serverId: $serverId, ') + ..write('conversationId: $conversationId') + ..write(')')) + .toString(); + } +} + +class $TombstonesTable extends Tombstones + with TableInfo<$TombstonesTable, TombstoneRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TombstonesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _tblMeta = const VerificationMeta('tbl'); + @override + late final GeneratedColumn tbl = GeneratedColumn( + 'tbl', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _rowIdMeta = const VerificationMeta('rowId'); + @override + late final GeneratedColumn rowId = GeneratedColumn( + 'row_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [tbl, rowId, deletedAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'tombstone'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('tbl')) { + context.handle( + _tblMeta, + tbl.isAcceptableOrUnknown(data['tbl']!, _tblMeta), + ); + } else if (isInserting) { + context.missing(_tblMeta); + } + if (data.containsKey('row_id')) { + context.handle( + _rowIdMeta, + rowId.isAcceptableOrUnknown(data['row_id']!, _rowIdMeta), + ); + } else if (isInserting) { + context.missing(_rowIdMeta); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } else if (isInserting) { + context.missing(_deletedAtMeta); + } + return context; + } + + @override + Set get $primaryKey => {tbl, rowId}; + @override + TombstoneRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TombstoneRow( + tbl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}tbl'], + )!, + rowId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}row_id'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}deleted_at'], + )!, + ); + } + + @override + $TombstonesTable createAlias(String alias) { + return $TombstonesTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class TombstoneRow extends DataClass implements Insertable { + final String tbl; + final String rowId; + final int deletedAt; + const TombstoneRow({ + required this.tbl, + required this.rowId, + required this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['tbl'] = Variable(tbl); + map['row_id'] = Variable(rowId); + map['deleted_at'] = Variable(deletedAt); + return map; + } + + TombstonesCompanion toCompanion(bool nullToAbsent) { + return TombstonesCompanion( + tbl: Value(tbl), + rowId: Value(rowId), + deletedAt: Value(deletedAt), + ); + } + + factory TombstoneRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TombstoneRow( + tbl: serializer.fromJson(json['tbl']), + rowId: serializer.fromJson(json['rowId']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'tbl': serializer.toJson(tbl), + 'rowId': serializer.toJson(rowId), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + TombstoneRow copyWith({String? tbl, String? rowId, int? deletedAt}) => + TombstoneRow( + tbl: tbl ?? this.tbl, + rowId: rowId ?? this.rowId, + deletedAt: deletedAt ?? this.deletedAt, + ); + TombstoneRow copyWithCompanion(TombstonesCompanion data) { + return TombstoneRow( + tbl: data.tbl.present ? data.tbl.value : this.tbl, + rowId: data.rowId.present ? data.rowId.value : this.rowId, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('TombstoneRow(') + ..write('tbl: $tbl, ') + ..write('rowId: $rowId, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(tbl, rowId, deletedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TombstoneRow && + other.tbl == this.tbl && + other.rowId == this.rowId && + other.deletedAt == this.deletedAt); +} + +class TombstonesCompanion extends UpdateCompanion { + final Value tbl; + final Value rowId; + final Value deletedAt; + const TombstonesCompanion({ + this.tbl = const Value.absent(), + this.rowId = const Value.absent(), + this.deletedAt = const Value.absent(), + }); + TombstonesCompanion.insert({ + required String tbl, + required String rowId, + required int deletedAt, + }) : tbl = Value(tbl), + rowId = Value(rowId), + deletedAt = Value(deletedAt); + static Insertable custom({ + Expression? tbl, + Expression? rowId, + Expression? deletedAt, + }) { + return RawValuesInsertable({ + if (tbl != null) 'tbl': tbl, + if (rowId != null) 'row_id': rowId, + if (deletedAt != null) 'deleted_at': deletedAt, + }); + } + + TombstonesCompanion copyWith({ + Value? tbl, + Value? rowId, + Value? deletedAt, + }) { + return TombstonesCompanion( + tbl: tbl ?? this.tbl, + rowId: rowId ?? this.rowId, + deletedAt: deletedAt ?? this.deletedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (tbl.present) { + map['tbl'] = Variable(tbl.value); + } + if (rowId.present) { + map['row_id'] = Variable(rowId.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TombstonesCompanion(') + ..write('tbl: $tbl, ') + ..write('rowId: $rowId, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } +} + +class $SyncStatesTable extends SyncStates + with TableInfo<$SyncStatesTable, SyncStateRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SyncStatesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _kMeta = const VerificationMeta('k'); + @override + late final GeneratedColumn k = GeneratedColumn( + 'k', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _vMeta = const VerificationMeta('v'); + @override + late final GeneratedColumn v = GeneratedColumn( + 'v', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [k, v]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'sync_state'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('k')) { + context.handle(_kMeta, k.isAcceptableOrUnknown(data['k']!, _kMeta)); + } else if (isInserting) { + context.missing(_kMeta); + } + if (data.containsKey('v')) { + context.handle(_vMeta, v.isAcceptableOrUnknown(data['v']!, _vMeta)); + } else if (isInserting) { + context.missing(_vMeta); + } + return context; + } + + @override + Set get $primaryKey => {k}; + @override + SyncStateRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SyncStateRow( + k: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}k'], + )!, + v: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}v'], + )!, + ); + } + + @override + $SyncStatesTable createAlias(String alias) { + return $SyncStatesTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; +} + +class SyncStateRow extends DataClass implements Insertable { + final String k; + final String v; + const SyncStateRow({required this.k, required this.v}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['k'] = Variable(k); + map['v'] = Variable(v); + return map; + } + + SyncStatesCompanion toCompanion(bool nullToAbsent) { + return SyncStatesCompanion(k: Value(k), v: Value(v)); + } + + factory SyncStateRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SyncStateRow( + k: serializer.fromJson(json['k']), + v: serializer.fromJson(json['v']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'k': serializer.toJson(k), + 'v': serializer.toJson(v), + }; + } + + SyncStateRow copyWith({String? k, String? v}) => + SyncStateRow(k: k ?? this.k, v: v ?? this.v); + SyncStateRow copyWithCompanion(SyncStatesCompanion data) { + return SyncStateRow( + k: data.k.present ? data.k.value : this.k, + v: data.v.present ? data.v.value : this.v, + ); + } + + @override + String toString() { + return (StringBuffer('SyncStateRow(') + ..write('k: $k, ') + ..write('v: $v') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(k, v); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SyncStateRow && other.k == this.k && other.v == this.v); +} + +class SyncStatesCompanion extends UpdateCompanion { + final Value k; + final Value v; + const SyncStatesCompanion({ + this.k = const Value.absent(), + this.v = const Value.absent(), + }); + SyncStatesCompanion.insert({required String k, required String v}) + : k = Value(k), + v = Value(v); + static Insertable custom({ + Expression? k, + Expression? v, + }) { + return RawValuesInsertable({if (k != null) 'k': k, if (v != null) 'v': v}); + } + + SyncStatesCompanion copyWith({Value? k, Value? v}) { + return SyncStatesCompanion(k: k ?? this.k, v: v ?? this.v); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (k.present) { + map['k'] = Variable(k.value); + } + if (v.present) { + map['v'] = Variable(v.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SyncStatesCompanion(') + ..write('k: $k, ') + ..write('v: $v') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDb extends GeneratedDatabase { + _$AppDb(QueryExecutor e) : super(e); + $AppDbManager get managers => $AppDbManager(this); + late final $PrivateKeysTable privateKeys = $PrivateKeysTable(this); + late final $ServersTable servers = $ServersTable(this); + late final $ServerTagsTable serverTags = $ServerTagsTable(this); + late final $ServerEnvsTable serverEnvs = $ServerEnvsTable(this); + late final $ServerJumpsTable serverJumps = $ServerJumpsTable(this); + late final $ServerDisabledCmdsTable serverDisabledCmds = + $ServerDisabledCmdsTable(this); + late final $ServerCustomCmdsTable serverCustomCmds = $ServerCustomCmdsTable( + this, + ); + late final $KnownHostsTable knownHosts = $KnownHostsTable(this); + late final $SnippetsTable snippets = $SnippetsTable(this); + late final $SnippetTagsTable snippetTags = $SnippetTagsTable(this); + late final $SnippetAutoRunOnTable snippetAutoRunOn = $SnippetAutoRunOnTable( + this, + ); + late final $PortForwardsTable portForwards = $PortForwardsTable(this); + late final $ContainerHostsTable containerHosts = $ContainerHostsTable(this); + late final $ContainerRuntimesTable containerRuntimes = + $ContainerRuntimesTable(this); + late final $ConnStatsTable connStats = $ConnStatsTable(this); + late final $AgentConversationsTable agentConversations = + $AgentConversationsTable(this); + late final $AgentActiveConversationsTable agentActiveConversations = + $AgentActiveConversationsTable(this); + late final $TombstonesTable tombstones = $TombstonesTable(this); + late final $SyncStatesTable syncStates = $SyncStatesTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + privateKeys, + servers, + serverTags, + serverEnvs, + serverJumps, + serverDisabledCmds, + serverCustomCmds, + knownHosts, + snippets, + snippetTags, + snippetAutoRunOn, + portForwards, + containerHosts, + containerRuntimes, + connStats, + agentConversations, + agentActiveConversations, + tombstones, + syncStates, + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ + WritePropagation( + on: TableUpdateQuery.onTableName( + 'private_key', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('server', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('server_tag', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('server_env', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('server_jump', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('server_jump', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('server_disabled_cmd', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('server_custom_cmd', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('known_host', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'snippet', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('snippet_tag', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'snippet', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('snippet_auto_run_on', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('snippet_auto_run_on', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('port_forward', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('container_host', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('container_runtime', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'server', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('conn_stat', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'agent_conversation', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('agent_active_conversation', kind: UpdateKind.delete), + ], + ), + ]); +} + +typedef $$PrivateKeysTableCreateCompanionBuilder = + PrivateKeysCompanion Function({ + Value updatedAt, + Value rev, + required String id, + required String name, + required String key, + }); +typedef $$PrivateKeysTableUpdateCompanionBuilder = + PrivateKeysCompanion Function({ + Value updatedAt, + Value rev, + Value id, + Value name, + Value key, + }); + +final class $$PrivateKeysTableReferences + extends BaseReferences<_$AppDb, $PrivateKeysTable, PrivateKeyRow> { + $$PrivateKeysTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$ServersTable, List> _serversRefsTable( + _$AppDb db, + ) => MultiTypedResultKey.fromTable( + db.servers, + aliasName: 'private_key__id__server__ssh_key_id', + ); + + $$ServersTableProcessedTableManager get serversRefs { + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.sshKeyId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_serversRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$PrivateKeysTableFilterComposer + extends Composer<_$AppDb, $PrivateKeysTable> { + $$PrivateKeysTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get rev => $composableBuilder( + column: $table.rev, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get key => $composableBuilder( + column: $table.key, + builder: (column) => ColumnFilters(column), + ); + + Expression serversRefs( + Expression Function($$ServersTableFilterComposer f) f, + ) { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.sshKeyId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$PrivateKeysTableOrderingComposer + extends Composer<_$AppDb, $PrivateKeysTable> { + $$PrivateKeysTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get key => $composableBuilder( + column: $table.key, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$PrivateKeysTableAnnotationComposer + extends Composer<_$AppDb, $PrivateKeysTable> { + $$PrivateKeysTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get rev => + $composableBuilder(column: $table.rev, builder: (column) => column); + + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get key => + $composableBuilder(column: $table.key, builder: (column) => column); + + Expression serversRefs( + Expression Function($$ServersTableAnnotationComposer a) f, + ) { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.sshKeyId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$PrivateKeysTableTableManager + extends + RootTableManager< + _$AppDb, + $PrivateKeysTable, + PrivateKeyRow, + $$PrivateKeysTableFilterComposer, + $$PrivateKeysTableOrderingComposer, + $$PrivateKeysTableAnnotationComposer, + $$PrivateKeysTableCreateCompanionBuilder, + $$PrivateKeysTableUpdateCompanionBuilder, + (PrivateKeyRow, $$PrivateKeysTableReferences), + PrivateKeyRow, + PrefetchHooks Function({bool serversRefs}) + > { + $$PrivateKeysTableTableManager(_$AppDb db, $PrivateKeysTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$PrivateKeysTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$PrivateKeysTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$PrivateKeysTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value updatedAt = const Value.absent(), + Value rev = const Value.absent(), + Value id = const Value.absent(), + Value name = const Value.absent(), + Value key = const Value.absent(), + }) => PrivateKeysCompanion( + updatedAt: updatedAt, + rev: rev, + id: id, + name: name, + key: key, + ), + createCompanionCallback: + ({ + Value updatedAt = const Value.absent(), + Value rev = const Value.absent(), + required String id, + required String name, + required String key, + }) => PrivateKeysCompanion.insert( + updatedAt: updatedAt, + rev: rev, + id: id, + name: name, + key: key, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$PrivateKeysTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serversRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (serversRefs) db.servers], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (serversRefs) + await $_getPrefetchedData< + PrivateKeyRow, + $PrivateKeysTable, + ServerRow + >( + currentTable: table, + referencedTable: $$PrivateKeysTableReferences + ._serversRefsTable(db), + managerFromTypedResult: (p0) => + $$PrivateKeysTableReferences( + db, + table, + p0, + ).serversRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where((e) => e.sshKeyId == item.id), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$PrivateKeysTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $PrivateKeysTable, + PrivateKeyRow, + $$PrivateKeysTableFilterComposer, + $$PrivateKeysTableOrderingComposer, + $$PrivateKeysTableAnnotationComposer, + $$PrivateKeysTableCreateCompanionBuilder, + $$PrivateKeysTableUpdateCompanionBuilder, + (PrivateKeyRow, $$PrivateKeysTableReferences), + PrivateKeyRow, + PrefetchHooks Function({bool serversRefs}) + >; +typedef $$ServersTableCreateCompanionBuilder = + ServersCompanion Function({ + Value updatedAt, + Value rev, + required String id, + required String name, + Value autoConnect, + Value systemType, + Value sshIp, + Value sshPort, + Value sshUser, + Value sshPwd, + Value sshKeyId, + Value sshKeyPath, + Value sshAlterUrl, + Value sshProxyCommand, + Value monitorAddr, + Value monitorUser, + Value monitorPwd, + Value monitorIgnoreCert, + Value wolMac, + Value wolIp, + Value wolPwd, + Value pveAddr, + Value pveIgnoreCert, + Value pvePwd, + Value preferTempDev, + Value tempIsCelsius, + Value logoUrl, + Value netDev, + Value scriptDir, + }); +typedef $$ServersTableUpdateCompanionBuilder = + ServersCompanion Function({ + Value updatedAt, + Value rev, + Value id, + Value name, + Value autoConnect, + Value systemType, + Value sshIp, + Value sshPort, + Value sshUser, + Value sshPwd, + Value sshKeyId, + Value sshKeyPath, + Value sshAlterUrl, + Value sshProxyCommand, + Value monitorAddr, + Value monitorUser, + Value monitorPwd, + Value monitorIgnoreCert, + Value wolMac, + Value wolIp, + Value wolPwd, + Value pveAddr, + Value pveIgnoreCert, + Value pvePwd, + Value preferTempDev, + Value tempIsCelsius, + Value logoUrl, + Value netDev, + Value scriptDir, + }); + +final class $$ServersTableReferences + extends BaseReferences<_$AppDb, $ServersTable, ServerRow> { + $$ServersTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $PrivateKeysTable _sshKeyIdTable(_$AppDb db) => + db.privateKeys.createAlias('server__ssh_key_id__private_key__id'); + + $$PrivateKeysTableProcessedTableManager? get sshKeyId { + final $_column = $_itemColumn('ssh_key_id'); + if ($_column == null) return null; + final manager = $$PrivateKeysTableTableManager( + $_db, + $_db.privateKeys, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_sshKeyIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$ServerTagsTable, List> + _serverTagsRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.serverTags, + aliasName: 'server__id__server_tag__server_id', + ); + + $$ServerTagsTableProcessedTableManager get serverTagsRefs { + final manager = $$ServerTagsTableTableManager( + $_db, + $_db.serverTags, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_serverTagsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$ServerEnvsTable, List> + _serverEnvsRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.serverEnvs, + aliasName: 'server__id__server_env__server_id', + ); + + $$ServerEnvsTableProcessedTableManager get serverEnvsRefs { + final manager = $$ServerEnvsTableTableManager( + $_db, + $_db.serverEnvs, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_serverEnvsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey< + $ServerDisabledCmdsTable, + List + > + _serverDisabledCmdsRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.serverDisabledCmds, + aliasName: 'server__id__server_disabled_cmd__server_id', + ); + + $$ServerDisabledCmdsTableProcessedTableManager get serverDisabledCmdsRefs { + final manager = $$ServerDisabledCmdsTableTableManager( + $_db, + $_db.serverDisabledCmds, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _serverDisabledCmdsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$ServerCustomCmdsTable, List> + _serverCustomCmdsRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.serverCustomCmds, + aliasName: 'server__id__server_custom_cmd__server_id', + ); + + $$ServerCustomCmdsTableProcessedTableManager get serverCustomCmdsRefs { + final manager = $$ServerCustomCmdsTableTableManager( + $_db, + $_db.serverCustomCmds, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _serverCustomCmdsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$KnownHostsTable, List> + _knownHostsRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.knownHosts, + aliasName: 'server__id__known_host__server_id', + ); + + $$KnownHostsTableProcessedTableManager get knownHostsRefs { + final manager = $$KnownHostsTableTableManager( + $_db, + $_db.knownHosts, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_knownHostsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$SnippetAutoRunOnTable, List> + _snippetAutoRunOnRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.snippetAutoRunOn, + aliasName: 'server__id__snippet_auto_run_on__server_id', + ); + + $$SnippetAutoRunOnTableProcessedTableManager get snippetAutoRunOnRefs { + final manager = $$SnippetAutoRunOnTableTableManager( + $_db, + $_db.snippetAutoRunOn, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _snippetAutoRunOnRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$PortForwardsTable, List> + _portForwardsRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.portForwards, + aliasName: 'server__id__port_forward__server_id', + ); + + $$PortForwardsTableProcessedTableManager get portForwardsRefs { + final manager = $$PortForwardsTableTableManager( + $_db, + $_db.portForwards, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_portForwardsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$ContainerHostsTable, List> + _containerHostsRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.containerHosts, + aliasName: 'server__id__container_host__server_id', + ); + + $$ContainerHostsTableProcessedTableManager get containerHostsRefs { + final manager = $$ContainerHostsTableTableManager( + $_db, + $_db.containerHosts, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_containerHostsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$ContainerRuntimesTable, List> + _containerRuntimesRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.containerRuntimes, + aliasName: 'server__id__container_runtime__server_id', + ); + + $$ContainerRuntimesTableProcessedTableManager get containerRuntimesRefs { + final manager = $$ContainerRuntimesTableTableManager( + $_db, + $_db.containerRuntimes, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _containerRuntimesRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$ConnStatsTable, List> + _connStatsRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.connStats, + aliasName: 'server__id__conn_stat__server_id', + ); + + $$ConnStatsTableProcessedTableManager get connStatsRefs { + final manager = $$ConnStatsTableTableManager( + $_db, + $_db.connStats, + ).filter((f) => f.serverId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_connStatsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$ServersTableFilterComposer extends Composer<_$AppDb, $ServersTable> { + $$ServersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get rev => $composableBuilder( + column: $table.rev, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get autoConnect => $composableBuilder( + column: $table.autoConnect, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get systemType => $composableBuilder( + column: $table.systemType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sshIp => $composableBuilder( + column: $table.sshIp, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sshPort => $composableBuilder( + column: $table.sshPort, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sshUser => $composableBuilder( + column: $table.sshUser, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sshPwd => $composableBuilder( + column: $table.sshPwd, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sshKeyPath => $composableBuilder( + column: $table.sshKeyPath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sshAlterUrl => $composableBuilder( + column: $table.sshAlterUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sshProxyCommand => $composableBuilder( + column: $table.sshProxyCommand, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get monitorAddr => $composableBuilder( + column: $table.monitorAddr, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get monitorUser => $composableBuilder( + column: $table.monitorUser, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get monitorPwd => $composableBuilder( + column: $table.monitorPwd, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get monitorIgnoreCert => $composableBuilder( + column: $table.monitorIgnoreCert, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get wolMac => $composableBuilder( + column: $table.wolMac, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get wolIp => $composableBuilder( + column: $table.wolIp, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get wolPwd => $composableBuilder( + column: $table.wolPwd, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get pveAddr => $composableBuilder( + column: $table.pveAddr, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get pveIgnoreCert => $composableBuilder( + column: $table.pveIgnoreCert, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get pvePwd => $composableBuilder( + column: $table.pvePwd, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get preferTempDev => $composableBuilder( + column: $table.preferTempDev, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get tempIsCelsius => $composableBuilder( + column: $table.tempIsCelsius, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get logoUrl => $composableBuilder( + column: $table.logoUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get netDev => $composableBuilder( + column: $table.netDev, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get scriptDir => $composableBuilder( + column: $table.scriptDir, + builder: (column) => ColumnFilters(column), + ); + + $$PrivateKeysTableFilterComposer get sshKeyId { + final $$PrivateKeysTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.sshKeyId, + referencedTable: $db.privateKeys, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PrivateKeysTableFilterComposer( + $db: $db, + $table: $db.privateKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression serverTagsRefs( + Expression Function($$ServerTagsTableFilterComposer f) f, + ) { + final $$ServerTagsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.serverTags, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServerTagsTableFilterComposer( + $db: $db, + $table: $db.serverTags, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression serverEnvsRefs( + Expression Function($$ServerEnvsTableFilterComposer f) f, + ) { + final $$ServerEnvsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.serverEnvs, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServerEnvsTableFilterComposer( + $db: $db, + $table: $db.serverEnvs, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression serverDisabledCmdsRefs( + Expression Function($$ServerDisabledCmdsTableFilterComposer f) f, + ) { + final $$ServerDisabledCmdsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.serverDisabledCmds, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServerDisabledCmdsTableFilterComposer( + $db: $db, + $table: $db.serverDisabledCmds, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression serverCustomCmdsRefs( + Expression Function($$ServerCustomCmdsTableFilterComposer f) f, + ) { + final $$ServerCustomCmdsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.serverCustomCmds, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServerCustomCmdsTableFilterComposer( + $db: $db, + $table: $db.serverCustomCmds, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression knownHostsRefs( + Expression Function($$KnownHostsTableFilterComposer f) f, + ) { + final $$KnownHostsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.knownHosts, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$KnownHostsTableFilterComposer( + $db: $db, + $table: $db.knownHosts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression snippetAutoRunOnRefs( + Expression Function($$SnippetAutoRunOnTableFilterComposer f) f, + ) { + final $$SnippetAutoRunOnTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.snippetAutoRunOn, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetAutoRunOnTableFilterComposer( + $db: $db, + $table: $db.snippetAutoRunOn, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression portForwardsRefs( + Expression Function($$PortForwardsTableFilterComposer f) f, + ) { + final $$PortForwardsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.portForwards, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PortForwardsTableFilterComposer( + $db: $db, + $table: $db.portForwards, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression containerHostsRefs( + Expression Function($$ContainerHostsTableFilterComposer f) f, + ) { + final $$ContainerHostsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.containerHosts, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ContainerHostsTableFilterComposer( + $db: $db, + $table: $db.containerHosts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression containerRuntimesRefs( + Expression Function($$ContainerRuntimesTableFilterComposer f) f, + ) { + final $$ContainerRuntimesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.containerRuntimes, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ContainerRuntimesTableFilterComposer( + $db: $db, + $table: $db.containerRuntimes, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression connStatsRefs( + Expression Function($$ConnStatsTableFilterComposer f) f, + ) { + final $$ConnStatsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.connStats, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ConnStatsTableFilterComposer( + $db: $db, + $table: $db.connStats, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$ServersTableOrderingComposer extends Composer<_$AppDb, $ServersTable> { + $$ServersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get autoConnect => $composableBuilder( + column: $table.autoConnect, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get systemType => $composableBuilder( + column: $table.systemType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sshIp => $composableBuilder( + column: $table.sshIp, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sshPort => $composableBuilder( + column: $table.sshPort, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sshUser => $composableBuilder( + column: $table.sshUser, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sshPwd => $composableBuilder( + column: $table.sshPwd, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sshKeyPath => $composableBuilder( + column: $table.sshKeyPath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sshAlterUrl => $composableBuilder( + column: $table.sshAlterUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sshProxyCommand => $composableBuilder( + column: $table.sshProxyCommand, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get monitorAddr => $composableBuilder( + column: $table.monitorAddr, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get monitorUser => $composableBuilder( + column: $table.monitorUser, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get monitorPwd => $composableBuilder( + column: $table.monitorPwd, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get monitorIgnoreCert => $composableBuilder( + column: $table.monitorIgnoreCert, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get wolMac => $composableBuilder( + column: $table.wolMac, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get wolIp => $composableBuilder( + column: $table.wolIp, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get wolPwd => $composableBuilder( + column: $table.wolPwd, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get pveAddr => $composableBuilder( + column: $table.pveAddr, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get pveIgnoreCert => $composableBuilder( + column: $table.pveIgnoreCert, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get pvePwd => $composableBuilder( + column: $table.pvePwd, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get preferTempDev => $composableBuilder( + column: $table.preferTempDev, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get tempIsCelsius => $composableBuilder( + column: $table.tempIsCelsius, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get logoUrl => $composableBuilder( + column: $table.logoUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get netDev => $composableBuilder( + column: $table.netDev, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get scriptDir => $composableBuilder( + column: $table.scriptDir, + builder: (column) => ColumnOrderings(column), + ); + + $$PrivateKeysTableOrderingComposer get sshKeyId { + final $$PrivateKeysTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.sshKeyId, + referencedTable: $db.privateKeys, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PrivateKeysTableOrderingComposer( + $db: $db, + $table: $db.privateKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServersTableAnnotationComposer + extends Composer<_$AppDb, $ServersTable> { + $$ServersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get rev => + $composableBuilder(column: $table.rev, builder: (column) => column); + + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get autoConnect => $composableBuilder( + column: $table.autoConnect, + builder: (column) => column, + ); + + GeneratedColumn get systemType => $composableBuilder( + column: $table.systemType, + builder: (column) => column, + ); + + GeneratedColumn get sshIp => + $composableBuilder(column: $table.sshIp, builder: (column) => column); + + GeneratedColumn get sshPort => + $composableBuilder(column: $table.sshPort, builder: (column) => column); + + GeneratedColumn get sshUser => + $composableBuilder(column: $table.sshUser, builder: (column) => column); + + GeneratedColumn get sshPwd => + $composableBuilder(column: $table.sshPwd, builder: (column) => column); + + GeneratedColumn get sshKeyPath => $composableBuilder( + column: $table.sshKeyPath, + builder: (column) => column, + ); + + GeneratedColumn get sshAlterUrl => $composableBuilder( + column: $table.sshAlterUrl, + builder: (column) => column, + ); + + GeneratedColumn get sshProxyCommand => $composableBuilder( + column: $table.sshProxyCommand, + builder: (column) => column, + ); + + GeneratedColumn get monitorAddr => $composableBuilder( + column: $table.monitorAddr, + builder: (column) => column, + ); + + GeneratedColumn get monitorUser => $composableBuilder( + column: $table.monitorUser, + builder: (column) => column, + ); + + GeneratedColumn get monitorPwd => $composableBuilder( + column: $table.monitorPwd, + builder: (column) => column, + ); + + GeneratedColumn get monitorIgnoreCert => $composableBuilder( + column: $table.monitorIgnoreCert, + builder: (column) => column, + ); + + GeneratedColumn get wolMac => + $composableBuilder(column: $table.wolMac, builder: (column) => column); + + GeneratedColumn get wolIp => + $composableBuilder(column: $table.wolIp, builder: (column) => column); + + GeneratedColumn get wolPwd => + $composableBuilder(column: $table.wolPwd, builder: (column) => column); + + GeneratedColumn get pveAddr => + $composableBuilder(column: $table.pveAddr, builder: (column) => column); + + GeneratedColumn get pveIgnoreCert => $composableBuilder( + column: $table.pveIgnoreCert, + builder: (column) => column, + ); + + GeneratedColumn get pvePwd => + $composableBuilder(column: $table.pvePwd, builder: (column) => column); + + GeneratedColumn get preferTempDev => $composableBuilder( + column: $table.preferTempDev, + builder: (column) => column, + ); + + GeneratedColumn get tempIsCelsius => $composableBuilder( + column: $table.tempIsCelsius, + builder: (column) => column, + ); + + GeneratedColumn get logoUrl => + $composableBuilder(column: $table.logoUrl, builder: (column) => column); + + GeneratedColumn get netDev => + $composableBuilder(column: $table.netDev, builder: (column) => column); + + GeneratedColumn get scriptDir => + $composableBuilder(column: $table.scriptDir, builder: (column) => column); + + $$PrivateKeysTableAnnotationComposer get sshKeyId { + final $$PrivateKeysTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.sshKeyId, + referencedTable: $db.privateKeys, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PrivateKeysTableAnnotationComposer( + $db: $db, + $table: $db.privateKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression serverTagsRefs( + Expression Function($$ServerTagsTableAnnotationComposer a) f, + ) { + final $$ServerTagsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.serverTags, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServerTagsTableAnnotationComposer( + $db: $db, + $table: $db.serverTags, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression serverEnvsRefs( + Expression Function($$ServerEnvsTableAnnotationComposer a) f, + ) { + final $$ServerEnvsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.serverEnvs, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServerEnvsTableAnnotationComposer( + $db: $db, + $table: $db.serverEnvs, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression serverDisabledCmdsRefs( + Expression Function($$ServerDisabledCmdsTableAnnotationComposer a) f, + ) { + final $$ServerDisabledCmdsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.serverDisabledCmds, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServerDisabledCmdsTableAnnotationComposer( + $db: $db, + $table: $db.serverDisabledCmds, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression serverCustomCmdsRefs( + Expression Function($$ServerCustomCmdsTableAnnotationComposer a) f, + ) { + final $$ServerCustomCmdsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.serverCustomCmds, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServerCustomCmdsTableAnnotationComposer( + $db: $db, + $table: $db.serverCustomCmds, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression knownHostsRefs( + Expression Function($$KnownHostsTableAnnotationComposer a) f, + ) { + final $$KnownHostsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.knownHosts, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$KnownHostsTableAnnotationComposer( + $db: $db, + $table: $db.knownHosts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression snippetAutoRunOnRefs( + Expression Function($$SnippetAutoRunOnTableAnnotationComposer a) f, + ) { + final $$SnippetAutoRunOnTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.snippetAutoRunOn, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetAutoRunOnTableAnnotationComposer( + $db: $db, + $table: $db.snippetAutoRunOn, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression portForwardsRefs( + Expression Function($$PortForwardsTableAnnotationComposer a) f, + ) { + final $$PortForwardsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.portForwards, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PortForwardsTableAnnotationComposer( + $db: $db, + $table: $db.portForwards, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression containerHostsRefs( + Expression Function($$ContainerHostsTableAnnotationComposer a) f, + ) { + final $$ContainerHostsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.containerHosts, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ContainerHostsTableAnnotationComposer( + $db: $db, + $table: $db.containerHosts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression containerRuntimesRefs( + Expression Function($$ContainerRuntimesTableAnnotationComposer a) f, + ) { + final $$ContainerRuntimesTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.containerRuntimes, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ContainerRuntimesTableAnnotationComposer( + $db: $db, + $table: $db.containerRuntimes, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression connStatsRefs( + Expression Function($$ConnStatsTableAnnotationComposer a) f, + ) { + final $$ConnStatsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.connStats, + getReferencedColumn: (t) => t.serverId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ConnStatsTableAnnotationComposer( + $db: $db, + $table: $db.connStats, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$ServersTableTableManager + extends + RootTableManager< + _$AppDb, + $ServersTable, + ServerRow, + $$ServersTableFilterComposer, + $$ServersTableOrderingComposer, + $$ServersTableAnnotationComposer, + $$ServersTableCreateCompanionBuilder, + $$ServersTableUpdateCompanionBuilder, + (ServerRow, $$ServersTableReferences), + ServerRow, + PrefetchHooks Function({ + bool sshKeyId, + bool serverTagsRefs, + bool serverEnvsRefs, + bool serverDisabledCmdsRefs, + bool serverCustomCmdsRefs, + bool knownHostsRefs, + bool snippetAutoRunOnRefs, + bool portForwardsRefs, + bool containerHostsRefs, + bool containerRuntimesRefs, + bool connStatsRefs, + }) + > { + $$ServersTableTableManager(_$AppDb db, $ServersTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ServersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ServersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ServersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value updatedAt = const Value.absent(), + Value rev = const Value.absent(), + Value id = const Value.absent(), + Value name = const Value.absent(), + Value autoConnect = const Value.absent(), + Value systemType = const Value.absent(), + Value sshIp = const Value.absent(), + Value sshPort = const Value.absent(), + Value sshUser = const Value.absent(), + Value sshPwd = const Value.absent(), + Value sshKeyId = const Value.absent(), + Value sshKeyPath = const Value.absent(), + Value sshAlterUrl = const Value.absent(), + Value sshProxyCommand = const Value.absent(), + Value monitorAddr = const Value.absent(), + Value monitorUser = const Value.absent(), + Value monitorPwd = const Value.absent(), + Value monitorIgnoreCert = const Value.absent(), + Value wolMac = const Value.absent(), + Value wolIp = const Value.absent(), + Value wolPwd = const Value.absent(), + Value pveAddr = const Value.absent(), + Value pveIgnoreCert = const Value.absent(), + Value pvePwd = const Value.absent(), + Value preferTempDev = const Value.absent(), + Value tempIsCelsius = const Value.absent(), + Value logoUrl = const Value.absent(), + Value netDev = const Value.absent(), + Value scriptDir = const Value.absent(), + }) => ServersCompanion( + updatedAt: updatedAt, + rev: rev, + id: id, + name: name, + autoConnect: autoConnect, + systemType: systemType, + sshIp: sshIp, + sshPort: sshPort, + sshUser: sshUser, + sshPwd: sshPwd, + sshKeyId: sshKeyId, + sshKeyPath: sshKeyPath, + sshAlterUrl: sshAlterUrl, + sshProxyCommand: sshProxyCommand, + monitorAddr: monitorAddr, + monitorUser: monitorUser, + monitorPwd: monitorPwd, + monitorIgnoreCert: monitorIgnoreCert, + wolMac: wolMac, + wolIp: wolIp, + wolPwd: wolPwd, + pveAddr: pveAddr, + pveIgnoreCert: pveIgnoreCert, + pvePwd: pvePwd, + preferTempDev: preferTempDev, + tempIsCelsius: tempIsCelsius, + logoUrl: logoUrl, + netDev: netDev, + scriptDir: scriptDir, + ), + createCompanionCallback: + ({ + Value updatedAt = const Value.absent(), + Value rev = const Value.absent(), + required String id, + required String name, + Value autoConnect = const Value.absent(), + Value systemType = const Value.absent(), + Value sshIp = const Value.absent(), + Value sshPort = const Value.absent(), + Value sshUser = const Value.absent(), + Value sshPwd = const Value.absent(), + Value sshKeyId = const Value.absent(), + Value sshKeyPath = const Value.absent(), + Value sshAlterUrl = const Value.absent(), + Value sshProxyCommand = const Value.absent(), + Value monitorAddr = const Value.absent(), + Value monitorUser = const Value.absent(), + Value monitorPwd = const Value.absent(), + Value monitorIgnoreCert = const Value.absent(), + Value wolMac = const Value.absent(), + Value wolIp = const Value.absent(), + Value wolPwd = const Value.absent(), + Value pveAddr = const Value.absent(), + Value pveIgnoreCert = const Value.absent(), + Value pvePwd = const Value.absent(), + Value preferTempDev = const Value.absent(), + Value tempIsCelsius = const Value.absent(), + Value logoUrl = const Value.absent(), + Value netDev = const Value.absent(), + Value scriptDir = const Value.absent(), + }) => ServersCompanion.insert( + updatedAt: updatedAt, + rev: rev, + id: id, + name: name, + autoConnect: autoConnect, + systemType: systemType, + sshIp: sshIp, + sshPort: sshPort, + sshUser: sshUser, + sshPwd: sshPwd, + sshKeyId: sshKeyId, + sshKeyPath: sshKeyPath, + sshAlterUrl: sshAlterUrl, + sshProxyCommand: sshProxyCommand, + monitorAddr: monitorAddr, + monitorUser: monitorUser, + monitorPwd: monitorPwd, + monitorIgnoreCert: monitorIgnoreCert, + wolMac: wolMac, + wolIp: wolIp, + wolPwd: wolPwd, + pveAddr: pveAddr, + pveIgnoreCert: pveIgnoreCert, + pvePwd: pvePwd, + preferTempDev: preferTempDev, + tempIsCelsius: tempIsCelsius, + logoUrl: logoUrl, + netDev: netDev, + scriptDir: scriptDir, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ServersTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + sshKeyId = false, + serverTagsRefs = false, + serverEnvsRefs = false, + serverDisabledCmdsRefs = false, + serverCustomCmdsRefs = false, + knownHostsRefs = false, + snippetAutoRunOnRefs = false, + portForwardsRefs = false, + containerHostsRefs = false, + containerRuntimesRefs = false, + connStatsRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (serverTagsRefs) db.serverTags, + if (serverEnvsRefs) db.serverEnvs, + if (serverDisabledCmdsRefs) db.serverDisabledCmds, + if (serverCustomCmdsRefs) db.serverCustomCmds, + if (knownHostsRefs) db.knownHosts, + if (snippetAutoRunOnRefs) db.snippetAutoRunOn, + if (portForwardsRefs) db.portForwards, + if (containerHostsRefs) db.containerHosts, + if (containerRuntimesRefs) db.containerRuntimes, + if (connStatsRefs) db.connStats, + ], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (sshKeyId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.sshKeyId, + referencedTable: $$ServersTableReferences + ._sshKeyIdTable(db), + referencedColumn: $$ServersTableReferences + ._sshKeyIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (serverTagsRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + ServerTagRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._serverTagsRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).serverTagsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + if (serverEnvsRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + ServerEnvRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._serverEnvsRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).serverEnvsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + if (serverDisabledCmdsRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + ServerDisabledCmdRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._serverDisabledCmdsRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).serverDisabledCmdsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + if (serverCustomCmdsRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + ServerCustomCmdRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._serverCustomCmdsRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).serverCustomCmdsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + if (knownHostsRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + KnownHostRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._knownHostsRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).knownHostsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + if (snippetAutoRunOnRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + SnippetAutoRunRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._snippetAutoRunOnRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).snippetAutoRunOnRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + if (portForwardsRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + PortForwardRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._portForwardsRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).portForwardsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + if (containerHostsRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + ContainerHostRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._containerHostsRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).containerHostsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + if (containerRuntimesRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + ContainerRuntimeRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._containerRuntimesRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).containerRuntimesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + if (connStatsRefs) + await $_getPrefetchedData< + ServerRow, + $ServersTable, + ConnStatRow + >( + currentTable: table, + referencedTable: $$ServersTableReferences + ._connStatsRefsTable(db), + managerFromTypedResult: (p0) => + $$ServersTableReferences( + db, + table, + p0, + ).connStatsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.serverId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$ServersTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $ServersTable, + ServerRow, + $$ServersTableFilterComposer, + $$ServersTableOrderingComposer, + $$ServersTableAnnotationComposer, + $$ServersTableCreateCompanionBuilder, + $$ServersTableUpdateCompanionBuilder, + (ServerRow, $$ServersTableReferences), + ServerRow, + PrefetchHooks Function({ + bool sshKeyId, + bool serverTagsRefs, + bool serverEnvsRefs, + bool serverDisabledCmdsRefs, + bool serverCustomCmdsRefs, + bool knownHostsRefs, + bool snippetAutoRunOnRefs, + bool portForwardsRefs, + bool containerHostsRefs, + bool containerRuntimesRefs, + bool connStatsRefs, + }) + >; +typedef $$ServerTagsTableCreateCompanionBuilder = + ServerTagsCompanion Function({ + required String serverId, + required String tag, + }); +typedef $$ServerTagsTableUpdateCompanionBuilder = + ServerTagsCompanion Function({Value serverId, Value tag}); + +final class $$ServerTagsTableReferences + extends BaseReferences<_$AppDb, $ServerTagsTable, ServerTagRow> { + $$ServerTagsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('server_tag__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ServerTagsTableFilterComposer + extends Composer<_$AppDb, $ServerTagsTable> { + $$ServerTagsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get tag => $composableBuilder( + column: $table.tag, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerTagsTableOrderingComposer + extends Composer<_$AppDb, $ServerTagsTable> { + $$ServerTagsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get tag => $composableBuilder( + column: $table.tag, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerTagsTableAnnotationComposer + extends Composer<_$AppDb, $ServerTagsTable> { + $$ServerTagsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get tag => + $composableBuilder(column: $table.tag, builder: (column) => column); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerTagsTableTableManager + extends + RootTableManager< + _$AppDb, + $ServerTagsTable, + ServerTagRow, + $$ServerTagsTableFilterComposer, + $$ServerTagsTableOrderingComposer, + $$ServerTagsTableAnnotationComposer, + $$ServerTagsTableCreateCompanionBuilder, + $$ServerTagsTableUpdateCompanionBuilder, + (ServerTagRow, $$ServerTagsTableReferences), + ServerTagRow, + PrefetchHooks Function({bool serverId}) + > { + $$ServerTagsTableTableManager(_$AppDb db, $ServerTagsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ServerTagsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ServerTagsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ServerTagsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value serverId = const Value.absent(), + Value tag = const Value.absent(), + }) => ServerTagsCompanion(serverId: serverId, tag: tag), + createCompanionCallback: + ({required String serverId, required String tag}) => + ServerTagsCompanion.insert(serverId: serverId, tag: tag), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ServerTagsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: $$ServerTagsTableReferences + ._serverIdTable(db), + referencedColumn: $$ServerTagsTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ServerTagsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $ServerTagsTable, + ServerTagRow, + $$ServerTagsTableFilterComposer, + $$ServerTagsTableOrderingComposer, + $$ServerTagsTableAnnotationComposer, + $$ServerTagsTableCreateCompanionBuilder, + $$ServerTagsTableUpdateCompanionBuilder, + (ServerTagRow, $$ServerTagsTableReferences), + ServerTagRow, + PrefetchHooks Function({bool serverId}) + >; +typedef $$ServerEnvsTableCreateCompanionBuilder = + ServerEnvsCompanion Function({ + required String serverId, + required String key, + required String value, + }); +typedef $$ServerEnvsTableUpdateCompanionBuilder = + ServerEnvsCompanion Function({ + Value serverId, + Value key, + Value value, + }); + +final class $$ServerEnvsTableReferences + extends BaseReferences<_$AppDb, $ServerEnvsTable, ServerEnvRow> { + $$ServerEnvsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('server_env__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ServerEnvsTableFilterComposer + extends Composer<_$AppDb, $ServerEnvsTable> { + $$ServerEnvsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get key => $composableBuilder( + column: $table.key, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get value => $composableBuilder( + column: $table.value, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerEnvsTableOrderingComposer + extends Composer<_$AppDb, $ServerEnvsTable> { + $$ServerEnvsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get key => $composableBuilder( + column: $table.key, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get value => $composableBuilder( + column: $table.value, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerEnvsTableAnnotationComposer + extends Composer<_$AppDb, $ServerEnvsTable> { + $$ServerEnvsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get key => + $composableBuilder(column: $table.key, builder: (column) => column); + + GeneratedColumn get value => + $composableBuilder(column: $table.value, builder: (column) => column); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerEnvsTableTableManager + extends + RootTableManager< + _$AppDb, + $ServerEnvsTable, + ServerEnvRow, + $$ServerEnvsTableFilterComposer, + $$ServerEnvsTableOrderingComposer, + $$ServerEnvsTableAnnotationComposer, + $$ServerEnvsTableCreateCompanionBuilder, + $$ServerEnvsTableUpdateCompanionBuilder, + (ServerEnvRow, $$ServerEnvsTableReferences), + ServerEnvRow, + PrefetchHooks Function({bool serverId}) + > { + $$ServerEnvsTableTableManager(_$AppDb db, $ServerEnvsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ServerEnvsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ServerEnvsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ServerEnvsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value serverId = const Value.absent(), + Value key = const Value.absent(), + Value value = const Value.absent(), + }) => ServerEnvsCompanion( + serverId: serverId, + key: key, + value: value, + ), + createCompanionCallback: + ({ + required String serverId, + required String key, + required String value, + }) => ServerEnvsCompanion.insert( + serverId: serverId, + key: key, + value: value, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ServerEnvsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: $$ServerEnvsTableReferences + ._serverIdTable(db), + referencedColumn: $$ServerEnvsTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ServerEnvsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $ServerEnvsTable, + ServerEnvRow, + $$ServerEnvsTableFilterComposer, + $$ServerEnvsTableOrderingComposer, + $$ServerEnvsTableAnnotationComposer, + $$ServerEnvsTableCreateCompanionBuilder, + $$ServerEnvsTableUpdateCompanionBuilder, + (ServerEnvRow, $$ServerEnvsTableReferences), + ServerEnvRow, + PrefetchHooks Function({bool serverId}) + >; +typedef $$ServerJumpsTableCreateCompanionBuilder = + ServerJumpsCompanion Function({ + required String serverId, + required int ord, + required String jumpId, + }); +typedef $$ServerJumpsTableUpdateCompanionBuilder = + ServerJumpsCompanion Function({ + Value serverId, + Value ord, + Value jumpId, + }); + +final class $$ServerJumpsTableReferences + extends BaseReferences<_$AppDb, $ServerJumpsTable, ServerJumpRow> { + $$ServerJumpsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('server_jump__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $ServersTable _jumpIdTable(_$AppDb db) => + db.servers.createAlias('server_jump__jump_id__server__id'); + + $$ServersTableProcessedTableManager get jumpId { + final $_column = $_itemColumn('jump_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_jumpIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ServerJumpsTableFilterComposer + extends Composer<_$AppDb, $ServerJumpsTable> { + $$ServerJumpsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get ord => $composableBuilder( + column: $table.ord, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ServersTableFilterComposer get jumpId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.jumpId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerJumpsTableOrderingComposer + extends Composer<_$AppDb, $ServerJumpsTable> { + $$ServerJumpsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get ord => $composableBuilder( + column: $table.ord, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ServersTableOrderingComposer get jumpId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.jumpId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerJumpsTableAnnotationComposer + extends Composer<_$AppDb, $ServerJumpsTable> { + $$ServerJumpsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get ord => + $composableBuilder(column: $table.ord, builder: (column) => column); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ServersTableAnnotationComposer get jumpId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.jumpId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerJumpsTableTableManager + extends + RootTableManager< + _$AppDb, + $ServerJumpsTable, + ServerJumpRow, + $$ServerJumpsTableFilterComposer, + $$ServerJumpsTableOrderingComposer, + $$ServerJumpsTableAnnotationComposer, + $$ServerJumpsTableCreateCompanionBuilder, + $$ServerJumpsTableUpdateCompanionBuilder, + (ServerJumpRow, $$ServerJumpsTableReferences), + ServerJumpRow, + PrefetchHooks Function({bool serverId, bool jumpId}) + > { + $$ServerJumpsTableTableManager(_$AppDb db, $ServerJumpsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ServerJumpsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ServerJumpsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ServerJumpsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value serverId = const Value.absent(), + Value ord = const Value.absent(), + Value jumpId = const Value.absent(), + }) => ServerJumpsCompanion( + serverId: serverId, + ord: ord, + jumpId: jumpId, + ), + createCompanionCallback: + ({ + required String serverId, + required int ord, + required String jumpId, + }) => ServerJumpsCompanion.insert( + serverId: serverId, + ord: ord, + jumpId: jumpId, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ServerJumpsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false, jumpId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: $$ServerJumpsTableReferences + ._serverIdTable(db), + referencedColumn: $$ServerJumpsTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + if (jumpId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.jumpId, + referencedTable: $$ServerJumpsTableReferences + ._jumpIdTable(db), + referencedColumn: $$ServerJumpsTableReferences + ._jumpIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ServerJumpsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $ServerJumpsTable, + ServerJumpRow, + $$ServerJumpsTableFilterComposer, + $$ServerJumpsTableOrderingComposer, + $$ServerJumpsTableAnnotationComposer, + $$ServerJumpsTableCreateCompanionBuilder, + $$ServerJumpsTableUpdateCompanionBuilder, + (ServerJumpRow, $$ServerJumpsTableReferences), + ServerJumpRow, + PrefetchHooks Function({bool serverId, bool jumpId}) + >; +typedef $$ServerDisabledCmdsTableCreateCompanionBuilder = + ServerDisabledCmdsCompanion Function({ + required String serverId, + required String cmdType, + }); +typedef $$ServerDisabledCmdsTableUpdateCompanionBuilder = + ServerDisabledCmdsCompanion Function({ + Value serverId, + Value cmdType, + }); + +final class $$ServerDisabledCmdsTableReferences + extends + BaseReferences< + _$AppDb, + $ServerDisabledCmdsTable, + ServerDisabledCmdRow + > { + $$ServerDisabledCmdsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('server_disabled_cmd__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ServerDisabledCmdsTableFilterComposer + extends Composer<_$AppDb, $ServerDisabledCmdsTable> { + $$ServerDisabledCmdsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get cmdType => $composableBuilder( + column: $table.cmdType, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerDisabledCmdsTableOrderingComposer + extends Composer<_$AppDb, $ServerDisabledCmdsTable> { + $$ServerDisabledCmdsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get cmdType => $composableBuilder( + column: $table.cmdType, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerDisabledCmdsTableAnnotationComposer + extends Composer<_$AppDb, $ServerDisabledCmdsTable> { + $$ServerDisabledCmdsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get cmdType => + $composableBuilder(column: $table.cmdType, builder: (column) => column); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerDisabledCmdsTableTableManager + extends + RootTableManager< + _$AppDb, + $ServerDisabledCmdsTable, + ServerDisabledCmdRow, + $$ServerDisabledCmdsTableFilterComposer, + $$ServerDisabledCmdsTableOrderingComposer, + $$ServerDisabledCmdsTableAnnotationComposer, + $$ServerDisabledCmdsTableCreateCompanionBuilder, + $$ServerDisabledCmdsTableUpdateCompanionBuilder, + (ServerDisabledCmdRow, $$ServerDisabledCmdsTableReferences), + ServerDisabledCmdRow, + PrefetchHooks Function({bool serverId}) + > { + $$ServerDisabledCmdsTableTableManager( + _$AppDb db, + $ServerDisabledCmdsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ServerDisabledCmdsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ServerDisabledCmdsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ServerDisabledCmdsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value serverId = const Value.absent(), + Value cmdType = const Value.absent(), + }) => ServerDisabledCmdsCompanion( + serverId: serverId, + cmdType: cmdType, + ), + createCompanionCallback: + ({required String serverId, required String cmdType}) => + ServerDisabledCmdsCompanion.insert( + serverId: serverId, + cmdType: cmdType, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ServerDisabledCmdsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: + $$ServerDisabledCmdsTableReferences + ._serverIdTable(db), + referencedColumn: + $$ServerDisabledCmdsTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ServerDisabledCmdsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $ServerDisabledCmdsTable, + ServerDisabledCmdRow, + $$ServerDisabledCmdsTableFilterComposer, + $$ServerDisabledCmdsTableOrderingComposer, + $$ServerDisabledCmdsTableAnnotationComposer, + $$ServerDisabledCmdsTableCreateCompanionBuilder, + $$ServerDisabledCmdsTableUpdateCompanionBuilder, + (ServerDisabledCmdRow, $$ServerDisabledCmdsTableReferences), + ServerDisabledCmdRow, + PrefetchHooks Function({bool serverId}) + >; +typedef $$ServerCustomCmdsTableCreateCompanionBuilder = + ServerCustomCmdsCompanion Function({ + required String serverId, + required String name, + required String cmd, + }); +typedef $$ServerCustomCmdsTableUpdateCompanionBuilder = + ServerCustomCmdsCompanion Function({ + Value serverId, + Value name, + Value cmd, + }); + +final class $$ServerCustomCmdsTableReferences + extends + BaseReferences<_$AppDb, $ServerCustomCmdsTable, ServerCustomCmdRow> { + $$ServerCustomCmdsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('server_custom_cmd__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ServerCustomCmdsTableFilterComposer + extends Composer<_$AppDb, $ServerCustomCmdsTable> { + $$ServerCustomCmdsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get cmd => $composableBuilder( + column: $table.cmd, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerCustomCmdsTableOrderingComposer + extends Composer<_$AppDb, $ServerCustomCmdsTable> { + $$ServerCustomCmdsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get cmd => $composableBuilder( + column: $table.cmd, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerCustomCmdsTableAnnotationComposer + extends Composer<_$AppDb, $ServerCustomCmdsTable> { + $$ServerCustomCmdsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get cmd => + $composableBuilder(column: $table.cmd, builder: (column) => column); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ServerCustomCmdsTableTableManager + extends + RootTableManager< + _$AppDb, + $ServerCustomCmdsTable, + ServerCustomCmdRow, + $$ServerCustomCmdsTableFilterComposer, + $$ServerCustomCmdsTableOrderingComposer, + $$ServerCustomCmdsTableAnnotationComposer, + $$ServerCustomCmdsTableCreateCompanionBuilder, + $$ServerCustomCmdsTableUpdateCompanionBuilder, + (ServerCustomCmdRow, $$ServerCustomCmdsTableReferences), + ServerCustomCmdRow, + PrefetchHooks Function({bool serverId}) + > { + $$ServerCustomCmdsTableTableManager(_$AppDb db, $ServerCustomCmdsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ServerCustomCmdsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ServerCustomCmdsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ServerCustomCmdsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value serverId = const Value.absent(), + Value name = const Value.absent(), + Value cmd = const Value.absent(), + }) => ServerCustomCmdsCompanion( + serverId: serverId, + name: name, + cmd: cmd, + ), + createCompanionCallback: + ({ + required String serverId, + required String name, + required String cmd, + }) => ServerCustomCmdsCompanion.insert( + serverId: serverId, + name: name, + cmd: cmd, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ServerCustomCmdsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: + $$ServerCustomCmdsTableReferences + ._serverIdTable(db), + referencedColumn: + $$ServerCustomCmdsTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ServerCustomCmdsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $ServerCustomCmdsTable, + ServerCustomCmdRow, + $$ServerCustomCmdsTableFilterComposer, + $$ServerCustomCmdsTableOrderingComposer, + $$ServerCustomCmdsTableAnnotationComposer, + $$ServerCustomCmdsTableCreateCompanionBuilder, + $$ServerCustomCmdsTableUpdateCompanionBuilder, + (ServerCustomCmdRow, $$ServerCustomCmdsTableReferences), + ServerCustomCmdRow, + PrefetchHooks Function({bool serverId}) + >; +typedef $$KnownHostsTableCreateCompanionBuilder = + KnownHostsCompanion Function({ + required String serverId, + required String keyType, + required String fingerprint, + }); +typedef $$KnownHostsTableUpdateCompanionBuilder = + KnownHostsCompanion Function({ + Value serverId, + Value keyType, + Value fingerprint, + }); + +final class $$KnownHostsTableReferences + extends BaseReferences<_$AppDb, $KnownHostsTable, KnownHostRow> { + $$KnownHostsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('known_host__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$KnownHostsTableFilterComposer + extends Composer<_$AppDb, $KnownHostsTable> { + $$KnownHostsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get keyType => $composableBuilder( + column: $table.keyType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get fingerprint => $composableBuilder( + column: $table.fingerprint, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$KnownHostsTableOrderingComposer + extends Composer<_$AppDb, $KnownHostsTable> { + $$KnownHostsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get keyType => $composableBuilder( + column: $table.keyType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get fingerprint => $composableBuilder( + column: $table.fingerprint, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$KnownHostsTableAnnotationComposer + extends Composer<_$AppDb, $KnownHostsTable> { + $$KnownHostsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get keyType => + $composableBuilder(column: $table.keyType, builder: (column) => column); + + GeneratedColumn get fingerprint => $composableBuilder( + column: $table.fingerprint, + builder: (column) => column, + ); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$KnownHostsTableTableManager + extends + RootTableManager< + _$AppDb, + $KnownHostsTable, + KnownHostRow, + $$KnownHostsTableFilterComposer, + $$KnownHostsTableOrderingComposer, + $$KnownHostsTableAnnotationComposer, + $$KnownHostsTableCreateCompanionBuilder, + $$KnownHostsTableUpdateCompanionBuilder, + (KnownHostRow, $$KnownHostsTableReferences), + KnownHostRow, + PrefetchHooks Function({bool serverId}) + > { + $$KnownHostsTableTableManager(_$AppDb db, $KnownHostsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$KnownHostsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$KnownHostsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$KnownHostsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value serverId = const Value.absent(), + Value keyType = const Value.absent(), + Value fingerprint = const Value.absent(), + }) => KnownHostsCompanion( + serverId: serverId, + keyType: keyType, + fingerprint: fingerprint, + ), + createCompanionCallback: + ({ + required String serverId, + required String keyType, + required String fingerprint, + }) => KnownHostsCompanion.insert( + serverId: serverId, + keyType: keyType, + fingerprint: fingerprint, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$KnownHostsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: $$KnownHostsTableReferences + ._serverIdTable(db), + referencedColumn: $$KnownHostsTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$KnownHostsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $KnownHostsTable, + KnownHostRow, + $$KnownHostsTableFilterComposer, + $$KnownHostsTableOrderingComposer, + $$KnownHostsTableAnnotationComposer, + $$KnownHostsTableCreateCompanionBuilder, + $$KnownHostsTableUpdateCompanionBuilder, + (KnownHostRow, $$KnownHostsTableReferences), + KnownHostRow, + PrefetchHooks Function({bool serverId}) + >; +typedef $$SnippetsTableCreateCompanionBuilder = + SnippetsCompanion Function({ + Value updatedAt, + Value rev, + required String id, + required String name, + required String script, + Value note, + }); +typedef $$SnippetsTableUpdateCompanionBuilder = + SnippetsCompanion Function({ + Value updatedAt, + Value rev, + Value id, + Value name, + Value script, + Value note, + }); + +final class $$SnippetsTableReferences + extends BaseReferences<_$AppDb, $SnippetsTable, SnippetRow> { + $$SnippetsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$SnippetTagsTable, List> + _snippetTagsRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.snippetTags, + aliasName: 'snippet__id__snippet_tag__snippet_id', + ); + + $$SnippetTagsTableProcessedTableManager get snippetTagsRefs { + final manager = $$SnippetTagsTableTableManager( + $_db, + $_db.snippetTags, + ).filter((f) => f.snippetId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_snippetTagsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$SnippetAutoRunOnTable, List> + _snippetAutoRunOnRefsTable(_$AppDb db) => MultiTypedResultKey.fromTable( + db.snippetAutoRunOn, + aliasName: 'snippet__id__snippet_auto_run_on__snippet_id', + ); + + $$SnippetAutoRunOnTableProcessedTableManager get snippetAutoRunOnRefs { + final manager = $$SnippetAutoRunOnTableTableManager( + $_db, + $_db.snippetAutoRunOn, + ).filter((f) => f.snippetId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _snippetAutoRunOnRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$SnippetsTableFilterComposer extends Composer<_$AppDb, $SnippetsTable> { + $$SnippetsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get rev => $composableBuilder( + column: $table.rev, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get script => $composableBuilder( + column: $table.script, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get note => $composableBuilder( + column: $table.note, + builder: (column) => ColumnFilters(column), + ); + + Expression snippetTagsRefs( + Expression Function($$SnippetTagsTableFilterComposer f) f, + ) { + final $$SnippetTagsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.snippetTags, + getReferencedColumn: (t) => t.snippetId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetTagsTableFilterComposer( + $db: $db, + $table: $db.snippetTags, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression snippetAutoRunOnRefs( + Expression Function($$SnippetAutoRunOnTableFilterComposer f) f, + ) { + final $$SnippetAutoRunOnTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.snippetAutoRunOn, + getReferencedColumn: (t) => t.snippetId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetAutoRunOnTableFilterComposer( + $db: $db, + $table: $db.snippetAutoRunOn, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$SnippetsTableOrderingComposer + extends Composer<_$AppDb, $SnippetsTable> { + $$SnippetsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get script => $composableBuilder( + column: $table.script, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get note => $composableBuilder( + column: $table.note, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$SnippetsTableAnnotationComposer + extends Composer<_$AppDb, $SnippetsTable> { + $$SnippetsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get rev => + $composableBuilder(column: $table.rev, builder: (column) => column); + + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get script => + $composableBuilder(column: $table.script, builder: (column) => column); + + GeneratedColumn get note => + $composableBuilder(column: $table.note, builder: (column) => column); + + Expression snippetTagsRefs( + Expression Function($$SnippetTagsTableAnnotationComposer a) f, + ) { + final $$SnippetTagsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.snippetTags, + getReferencedColumn: (t) => t.snippetId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetTagsTableAnnotationComposer( + $db: $db, + $table: $db.snippetTags, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression snippetAutoRunOnRefs( + Expression Function($$SnippetAutoRunOnTableAnnotationComposer a) f, + ) { + final $$SnippetAutoRunOnTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.snippetAutoRunOn, + getReferencedColumn: (t) => t.snippetId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetAutoRunOnTableAnnotationComposer( + $db: $db, + $table: $db.snippetAutoRunOn, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$SnippetsTableTableManager + extends + RootTableManager< + _$AppDb, + $SnippetsTable, + SnippetRow, + $$SnippetsTableFilterComposer, + $$SnippetsTableOrderingComposer, + $$SnippetsTableAnnotationComposer, + $$SnippetsTableCreateCompanionBuilder, + $$SnippetsTableUpdateCompanionBuilder, + (SnippetRow, $$SnippetsTableReferences), + SnippetRow, + PrefetchHooks Function({ + bool snippetTagsRefs, + bool snippetAutoRunOnRefs, + }) + > { + $$SnippetsTableTableManager(_$AppDb db, $SnippetsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SnippetsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SnippetsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SnippetsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value updatedAt = const Value.absent(), + Value rev = const Value.absent(), + Value id = const Value.absent(), + Value name = const Value.absent(), + Value script = const Value.absent(), + Value note = const Value.absent(), + }) => SnippetsCompanion( + updatedAt: updatedAt, + rev: rev, + id: id, + name: name, + script: script, + note: note, + ), + createCompanionCallback: + ({ + Value updatedAt = const Value.absent(), + Value rev = const Value.absent(), + required String id, + required String name, + required String script, + Value note = const Value.absent(), + }) => SnippetsCompanion.insert( + updatedAt: updatedAt, + rev: rev, + id: id, + name: name, + script: script, + note: note, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$SnippetsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({snippetTagsRefs = false, snippetAutoRunOnRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (snippetTagsRefs) db.snippetTags, + if (snippetAutoRunOnRefs) db.snippetAutoRunOn, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (snippetTagsRefs) + await $_getPrefetchedData< + SnippetRow, + $SnippetsTable, + SnippetTagRow + >( + currentTable: table, + referencedTable: $$SnippetsTableReferences + ._snippetTagsRefsTable(db), + managerFromTypedResult: (p0) => + $$SnippetsTableReferences( + db, + table, + p0, + ).snippetTagsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.snippetId == item.id, + ), + typedResults: items, + ), + if (snippetAutoRunOnRefs) + await $_getPrefetchedData< + SnippetRow, + $SnippetsTable, + SnippetAutoRunRow + >( + currentTable: table, + referencedTable: $$SnippetsTableReferences + ._snippetAutoRunOnRefsTable(db), + managerFromTypedResult: (p0) => + $$SnippetsTableReferences( + db, + table, + p0, + ).snippetAutoRunOnRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.snippetId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$SnippetsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $SnippetsTable, + SnippetRow, + $$SnippetsTableFilterComposer, + $$SnippetsTableOrderingComposer, + $$SnippetsTableAnnotationComposer, + $$SnippetsTableCreateCompanionBuilder, + $$SnippetsTableUpdateCompanionBuilder, + (SnippetRow, $$SnippetsTableReferences), + SnippetRow, + PrefetchHooks Function({bool snippetTagsRefs, bool snippetAutoRunOnRefs}) + >; +typedef $$SnippetTagsTableCreateCompanionBuilder = + SnippetTagsCompanion Function({ + required String snippetId, + required String tag, + }); +typedef $$SnippetTagsTableUpdateCompanionBuilder = + SnippetTagsCompanion Function({Value snippetId, Value tag}); + +final class $$SnippetTagsTableReferences + extends BaseReferences<_$AppDb, $SnippetTagsTable, SnippetTagRow> { + $$SnippetTagsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $SnippetsTable _snippetIdTable(_$AppDb db) => + db.snippets.createAlias('snippet_tag__snippet_id__snippet__id'); + + $$SnippetsTableProcessedTableManager get snippetId { + final $_column = $_itemColumn('snippet_id')!; + + final manager = $$SnippetsTableTableManager( + $_db, + $_db.snippets, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_snippetIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$SnippetTagsTableFilterComposer + extends Composer<_$AppDb, $SnippetTagsTable> { + $$SnippetTagsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get tag => $composableBuilder( + column: $table.tag, + builder: (column) => ColumnFilters(column), + ); + + $$SnippetsTableFilterComposer get snippetId { + final $$SnippetsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.snippetId, + referencedTable: $db.snippets, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetsTableFilterComposer( + $db: $db, + $table: $db.snippets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$SnippetTagsTableOrderingComposer + extends Composer<_$AppDb, $SnippetTagsTable> { + $$SnippetTagsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get tag => $composableBuilder( + column: $table.tag, + builder: (column) => ColumnOrderings(column), + ); + + $$SnippetsTableOrderingComposer get snippetId { + final $$SnippetsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.snippetId, + referencedTable: $db.snippets, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetsTableOrderingComposer( + $db: $db, + $table: $db.snippets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$SnippetTagsTableAnnotationComposer + extends Composer<_$AppDb, $SnippetTagsTable> { + $$SnippetTagsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get tag => + $composableBuilder(column: $table.tag, builder: (column) => column); + + $$SnippetsTableAnnotationComposer get snippetId { + final $$SnippetsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.snippetId, + referencedTable: $db.snippets, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetsTableAnnotationComposer( + $db: $db, + $table: $db.snippets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$SnippetTagsTableTableManager + extends + RootTableManager< + _$AppDb, + $SnippetTagsTable, + SnippetTagRow, + $$SnippetTagsTableFilterComposer, + $$SnippetTagsTableOrderingComposer, + $$SnippetTagsTableAnnotationComposer, + $$SnippetTagsTableCreateCompanionBuilder, + $$SnippetTagsTableUpdateCompanionBuilder, + (SnippetTagRow, $$SnippetTagsTableReferences), + SnippetTagRow, + PrefetchHooks Function({bool snippetId}) + > { + $$SnippetTagsTableTableManager(_$AppDb db, $SnippetTagsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SnippetTagsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SnippetTagsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SnippetTagsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value snippetId = const Value.absent(), + Value tag = const Value.absent(), + }) => SnippetTagsCompanion(snippetId: snippetId, tag: tag), + createCompanionCallback: + ({required String snippetId, required String tag}) => + SnippetTagsCompanion.insert(snippetId: snippetId, tag: tag), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$SnippetTagsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({snippetId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (snippetId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.snippetId, + referencedTable: $$SnippetTagsTableReferences + ._snippetIdTable(db), + referencedColumn: $$SnippetTagsTableReferences + ._snippetIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$SnippetTagsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $SnippetTagsTable, + SnippetTagRow, + $$SnippetTagsTableFilterComposer, + $$SnippetTagsTableOrderingComposer, + $$SnippetTagsTableAnnotationComposer, + $$SnippetTagsTableCreateCompanionBuilder, + $$SnippetTagsTableUpdateCompanionBuilder, + (SnippetTagRow, $$SnippetTagsTableReferences), + SnippetTagRow, + PrefetchHooks Function({bool snippetId}) + >; +typedef $$SnippetAutoRunOnTableCreateCompanionBuilder = + SnippetAutoRunOnCompanion Function({ + required String snippetId, + required String serverId, + }); +typedef $$SnippetAutoRunOnTableUpdateCompanionBuilder = + SnippetAutoRunOnCompanion Function({ + Value snippetId, + Value serverId, + }); + +final class $$SnippetAutoRunOnTableReferences + extends BaseReferences<_$AppDb, $SnippetAutoRunOnTable, SnippetAutoRunRow> { + $$SnippetAutoRunOnTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $SnippetsTable _snippetIdTable(_$AppDb db) => + db.snippets.createAlias('snippet_auto_run_on__snippet_id__snippet__id'); + + $$SnippetsTableProcessedTableManager get snippetId { + final $_column = $_itemColumn('snippet_id')!; + + final manager = $$SnippetsTableTableManager( + $_db, + $_db.snippets, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_snippetIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('snippet_auto_run_on__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$SnippetAutoRunOnTableFilterComposer + extends Composer<_$AppDb, $SnippetAutoRunOnTable> { + $$SnippetAutoRunOnTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$SnippetsTableFilterComposer get snippetId { + final $$SnippetsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.snippetId, + referencedTable: $db.snippets, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetsTableFilterComposer( + $db: $db, + $table: $db.snippets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$SnippetAutoRunOnTableOrderingComposer + extends Composer<_$AppDb, $SnippetAutoRunOnTable> { + $$SnippetAutoRunOnTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$SnippetsTableOrderingComposer get snippetId { + final $$SnippetsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.snippetId, + referencedTable: $db.snippets, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetsTableOrderingComposer( + $db: $db, + $table: $db.snippets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$SnippetAutoRunOnTableAnnotationComposer + extends Composer<_$AppDb, $SnippetAutoRunOnTable> { + $$SnippetAutoRunOnTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$SnippetsTableAnnotationComposer get snippetId { + final $$SnippetsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.snippetId, + referencedTable: $db.snippets, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SnippetsTableAnnotationComposer( + $db: $db, + $table: $db.snippets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$SnippetAutoRunOnTableTableManager + extends + RootTableManager< + _$AppDb, + $SnippetAutoRunOnTable, + SnippetAutoRunRow, + $$SnippetAutoRunOnTableFilterComposer, + $$SnippetAutoRunOnTableOrderingComposer, + $$SnippetAutoRunOnTableAnnotationComposer, + $$SnippetAutoRunOnTableCreateCompanionBuilder, + $$SnippetAutoRunOnTableUpdateCompanionBuilder, + (SnippetAutoRunRow, $$SnippetAutoRunOnTableReferences), + SnippetAutoRunRow, + PrefetchHooks Function({bool snippetId, bool serverId}) + > { + $$SnippetAutoRunOnTableTableManager(_$AppDb db, $SnippetAutoRunOnTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SnippetAutoRunOnTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SnippetAutoRunOnTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SnippetAutoRunOnTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value snippetId = const Value.absent(), + Value serverId = const Value.absent(), + }) => SnippetAutoRunOnCompanion( + snippetId: snippetId, + serverId: serverId, + ), + createCompanionCallback: + ({required String snippetId, required String serverId}) => + SnippetAutoRunOnCompanion.insert( + snippetId: snippetId, + serverId: serverId, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$SnippetAutoRunOnTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({snippetId = false, serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (snippetId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.snippetId, + referencedTable: + $$SnippetAutoRunOnTableReferences + ._snippetIdTable(db), + referencedColumn: + $$SnippetAutoRunOnTableReferences + ._snippetIdTable(db) + .id, + ) + as T; + } + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: + $$SnippetAutoRunOnTableReferences + ._serverIdTable(db), + referencedColumn: + $$SnippetAutoRunOnTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$SnippetAutoRunOnTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $SnippetAutoRunOnTable, + SnippetAutoRunRow, + $$SnippetAutoRunOnTableFilterComposer, + $$SnippetAutoRunOnTableOrderingComposer, + $$SnippetAutoRunOnTableAnnotationComposer, + $$SnippetAutoRunOnTableCreateCompanionBuilder, + $$SnippetAutoRunOnTableUpdateCompanionBuilder, + (SnippetAutoRunRow, $$SnippetAutoRunOnTableReferences), + SnippetAutoRunRow, + PrefetchHooks Function({bool snippetId, bool serverId}) + >; +typedef $$PortForwardsTableCreateCompanionBuilder = + PortForwardsCompanion Function({ + Value updatedAt, + Value rev, + required String id, + required String serverId, + required String name, + required String type, + Value localHost, + Value localPort, + Value remoteHost, + Value remotePort, + }); +typedef $$PortForwardsTableUpdateCompanionBuilder = + PortForwardsCompanion Function({ + Value updatedAt, + Value rev, + Value id, + Value serverId, + Value name, + Value type, + Value localHost, + Value localPort, + Value remoteHost, + Value remotePort, + }); + +final class $$PortForwardsTableReferences + extends BaseReferences<_$AppDb, $PortForwardsTable, PortForwardRow> { + $$PortForwardsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('port_forward__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$PortForwardsTableFilterComposer + extends Composer<_$AppDb, $PortForwardsTable> { + $$PortForwardsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get rev => $composableBuilder( + column: $table.rev, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localHost => $composableBuilder( + column: $table.localHost, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localPort => $composableBuilder( + column: $table.localPort, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get remoteHost => $composableBuilder( + column: $table.remoteHost, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get remotePort => $composableBuilder( + column: $table.remotePort, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PortForwardsTableOrderingComposer + extends Composer<_$AppDb, $PortForwardsTable> { + $$PortForwardsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localHost => $composableBuilder( + column: $table.localHost, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localPort => $composableBuilder( + column: $table.localPort, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get remoteHost => $composableBuilder( + column: $table.remoteHost, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get remotePort => $composableBuilder( + column: $table.remotePort, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PortForwardsTableAnnotationComposer + extends Composer<_$AppDb, $PortForwardsTable> { + $$PortForwardsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get rev => + $composableBuilder(column: $table.rev, builder: (column) => column); + + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get localHost => + $composableBuilder(column: $table.localHost, builder: (column) => column); + + GeneratedColumn get localPort => + $composableBuilder(column: $table.localPort, builder: (column) => column); + + GeneratedColumn get remoteHost => $composableBuilder( + column: $table.remoteHost, + builder: (column) => column, + ); + + GeneratedColumn get remotePort => $composableBuilder( + column: $table.remotePort, + builder: (column) => column, + ); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PortForwardsTableTableManager + extends + RootTableManager< + _$AppDb, + $PortForwardsTable, + PortForwardRow, + $$PortForwardsTableFilterComposer, + $$PortForwardsTableOrderingComposer, + $$PortForwardsTableAnnotationComposer, + $$PortForwardsTableCreateCompanionBuilder, + $$PortForwardsTableUpdateCompanionBuilder, + (PortForwardRow, $$PortForwardsTableReferences), + PortForwardRow, + PrefetchHooks Function({bool serverId}) + > { + $$PortForwardsTableTableManager(_$AppDb db, $PortForwardsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$PortForwardsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$PortForwardsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$PortForwardsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value updatedAt = const Value.absent(), + Value rev = const Value.absent(), + Value id = const Value.absent(), + Value serverId = const Value.absent(), + Value name = const Value.absent(), + Value type = const Value.absent(), + Value localHost = const Value.absent(), + Value localPort = const Value.absent(), + Value remoteHost = const Value.absent(), + Value remotePort = const Value.absent(), + }) => PortForwardsCompanion( + updatedAt: updatedAt, + rev: rev, + id: id, + serverId: serverId, + name: name, + type: type, + localHost: localHost, + localPort: localPort, + remoteHost: remoteHost, + remotePort: remotePort, + ), + createCompanionCallback: + ({ + Value updatedAt = const Value.absent(), + Value rev = const Value.absent(), + required String id, + required String serverId, + required String name, + required String type, + Value localHost = const Value.absent(), + Value localPort = const Value.absent(), + Value remoteHost = const Value.absent(), + Value remotePort = const Value.absent(), + }) => PortForwardsCompanion.insert( + updatedAt: updatedAt, + rev: rev, + id: id, + serverId: serverId, + name: name, + type: type, + localHost: localHost, + localPort: localPort, + remoteHost: remoteHost, + remotePort: remotePort, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$PortForwardsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: $$PortForwardsTableReferences + ._serverIdTable(db), + referencedColumn: $$PortForwardsTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$PortForwardsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $PortForwardsTable, + PortForwardRow, + $$PortForwardsTableFilterComposer, + $$PortForwardsTableOrderingComposer, + $$PortForwardsTableAnnotationComposer, + $$PortForwardsTableCreateCompanionBuilder, + $$PortForwardsTableUpdateCompanionBuilder, + (PortForwardRow, $$PortForwardsTableReferences), + PortForwardRow, + PrefetchHooks Function({bool serverId}) + >; +typedef $$ContainerHostsTableCreateCompanionBuilder = + ContainerHostsCompanion Function({ + required String serverId, + required String type, + required String host, + }); +typedef $$ContainerHostsTableUpdateCompanionBuilder = + ContainerHostsCompanion Function({ + Value serverId, + Value type, + Value host, + }); + +final class $$ContainerHostsTableReferences + extends BaseReferences<_$AppDb, $ContainerHostsTable, ContainerHostRow> { + $$ContainerHostsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('container_host__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ContainerHostsTableFilterComposer + extends Composer<_$AppDb, $ContainerHostsTable> { + $$ContainerHostsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get host => $composableBuilder( + column: $table.host, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ContainerHostsTableOrderingComposer + extends Composer<_$AppDb, $ContainerHostsTable> { + $$ContainerHostsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get host => $composableBuilder( + column: $table.host, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ContainerHostsTableAnnotationComposer + extends Composer<_$AppDb, $ContainerHostsTable> { + $$ContainerHostsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get host => + $composableBuilder(column: $table.host, builder: (column) => column); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ContainerHostsTableTableManager + extends + RootTableManager< + _$AppDb, + $ContainerHostsTable, + ContainerHostRow, + $$ContainerHostsTableFilterComposer, + $$ContainerHostsTableOrderingComposer, + $$ContainerHostsTableAnnotationComposer, + $$ContainerHostsTableCreateCompanionBuilder, + $$ContainerHostsTableUpdateCompanionBuilder, + (ContainerHostRow, $$ContainerHostsTableReferences), + ContainerHostRow, + PrefetchHooks Function({bool serverId}) + > { + $$ContainerHostsTableTableManager(_$AppDb db, $ContainerHostsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ContainerHostsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ContainerHostsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ContainerHostsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value serverId = const Value.absent(), + Value type = const Value.absent(), + Value host = const Value.absent(), + }) => ContainerHostsCompanion( + serverId: serverId, + type: type, + host: host, + ), + createCompanionCallback: + ({ + required String serverId, + required String type, + required String host, + }) => ContainerHostsCompanion.insert( + serverId: serverId, + type: type, + host: host, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ContainerHostsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: $$ContainerHostsTableReferences + ._serverIdTable(db), + referencedColumn: + $$ContainerHostsTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ContainerHostsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $ContainerHostsTable, + ContainerHostRow, + $$ContainerHostsTableFilterComposer, + $$ContainerHostsTableOrderingComposer, + $$ContainerHostsTableAnnotationComposer, + $$ContainerHostsTableCreateCompanionBuilder, + $$ContainerHostsTableUpdateCompanionBuilder, + (ContainerHostRow, $$ContainerHostsTableReferences), + ContainerHostRow, + PrefetchHooks Function({bool serverId}) + >; +typedef $$ContainerRuntimesTableCreateCompanionBuilder = + ContainerRuntimesCompanion Function({ + required String serverId, + required String type, + }); +typedef $$ContainerRuntimesTableUpdateCompanionBuilder = + ContainerRuntimesCompanion Function({ + Value serverId, + Value type, + }); + +final class $$ContainerRuntimesTableReferences + extends + BaseReferences<_$AppDb, $ContainerRuntimesTable, ContainerRuntimeRow> { + $$ContainerRuntimesTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('container_runtime__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ContainerRuntimesTableFilterComposer + extends Composer<_$AppDb, $ContainerRuntimesTable> { + $$ContainerRuntimesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ContainerRuntimesTableOrderingComposer + extends Composer<_$AppDb, $ContainerRuntimesTable> { + $$ContainerRuntimesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ContainerRuntimesTableAnnotationComposer + extends Composer<_$AppDb, $ContainerRuntimesTable> { + $$ContainerRuntimesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ContainerRuntimesTableTableManager + extends + RootTableManager< + _$AppDb, + $ContainerRuntimesTable, + ContainerRuntimeRow, + $$ContainerRuntimesTableFilterComposer, + $$ContainerRuntimesTableOrderingComposer, + $$ContainerRuntimesTableAnnotationComposer, + $$ContainerRuntimesTableCreateCompanionBuilder, + $$ContainerRuntimesTableUpdateCompanionBuilder, + (ContainerRuntimeRow, $$ContainerRuntimesTableReferences), + ContainerRuntimeRow, + PrefetchHooks Function({bool serverId}) + > { + $$ContainerRuntimesTableTableManager( + _$AppDb db, + $ContainerRuntimesTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ContainerRuntimesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ContainerRuntimesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ContainerRuntimesTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value serverId = const Value.absent(), + Value type = const Value.absent(), + }) => ContainerRuntimesCompanion(serverId: serverId, type: type), + createCompanionCallback: + ({required String serverId, required String type}) => + ContainerRuntimesCompanion.insert( + serverId: serverId, + type: type, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ContainerRuntimesTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: + $$ContainerRuntimesTableReferences + ._serverIdTable(db), + referencedColumn: + $$ContainerRuntimesTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ContainerRuntimesTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $ContainerRuntimesTable, + ContainerRuntimeRow, + $$ContainerRuntimesTableFilterComposer, + $$ContainerRuntimesTableOrderingComposer, + $$ContainerRuntimesTableAnnotationComposer, + $$ContainerRuntimesTableCreateCompanionBuilder, + $$ContainerRuntimesTableUpdateCompanionBuilder, + (ContainerRuntimeRow, $$ContainerRuntimesTableReferences), + ContainerRuntimeRow, + PrefetchHooks Function({bool serverId}) + >; +typedef $$ConnStatsTableCreateCompanionBuilder = + ConnStatsCompanion Function({ + required String id, + required String serverId, + required String serverName, + required int timestamp, + required String result, + Value errorMessage, + required int durationMs, + }); +typedef $$ConnStatsTableUpdateCompanionBuilder = + ConnStatsCompanion Function({ + Value id, + Value serverId, + Value serverName, + Value timestamp, + Value result, + Value errorMessage, + Value durationMs, + }); + +final class $$ConnStatsTableReferences + extends BaseReferences<_$AppDb, $ConnStatsTable, ConnStatRow> { + $$ConnStatsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ServersTable _serverIdTable(_$AppDb db) => + db.servers.createAlias('conn_stat__server_id__server__id'); + + $$ServersTableProcessedTableManager get serverId { + final $_column = $_itemColumn('server_id')!; + + final manager = $$ServersTableTableManager( + $_db, + $_db.servers, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_serverIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ConnStatsTableFilterComposer + extends Composer<_$AppDb, $ConnStatsTable> { + $$ConnStatsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverName => $composableBuilder( + column: $table.serverName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get timestamp => $composableBuilder( + column: $table.timestamp, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get result => $composableBuilder( + column: $table.result, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get durationMs => $composableBuilder( + column: $table.durationMs, + builder: (column) => ColumnFilters(column), + ); + + $$ServersTableFilterComposer get serverId { + final $$ServersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableFilterComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ConnStatsTableOrderingComposer + extends Composer<_$AppDb, $ConnStatsTable> { + $$ConnStatsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverName => $composableBuilder( + column: $table.serverName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get timestamp => $composableBuilder( + column: $table.timestamp, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get result => $composableBuilder( + column: $table.result, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get durationMs => $composableBuilder( + column: $table.durationMs, + builder: (column) => ColumnOrderings(column), + ); + + $$ServersTableOrderingComposer get serverId { + final $$ServersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableOrderingComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ConnStatsTableAnnotationComposer + extends Composer<_$AppDb, $ConnStatsTable> { + $$ConnStatsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get serverName => $composableBuilder( + column: $table.serverName, + builder: (column) => column, + ); + + GeneratedColumn get timestamp => + $composableBuilder(column: $table.timestamp, builder: (column) => column); + + GeneratedColumn get result => + $composableBuilder(column: $table.result, builder: (column) => column); + + GeneratedColumn get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => column, + ); + + GeneratedColumn get durationMs => $composableBuilder( + column: $table.durationMs, + builder: (column) => column, + ); + + $$ServersTableAnnotationComposer get serverId { + final $$ServersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.serverId, + referencedTable: $db.servers, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ServersTableAnnotationComposer( + $db: $db, + $table: $db.servers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ConnStatsTableTableManager + extends + RootTableManager< + _$AppDb, + $ConnStatsTable, + ConnStatRow, + $$ConnStatsTableFilterComposer, + $$ConnStatsTableOrderingComposer, + $$ConnStatsTableAnnotationComposer, + $$ConnStatsTableCreateCompanionBuilder, + $$ConnStatsTableUpdateCompanionBuilder, + (ConnStatRow, $$ConnStatsTableReferences), + ConnStatRow, + PrefetchHooks Function({bool serverId}) + > { + $$ConnStatsTableTableManager(_$AppDb db, $ConnStatsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ConnStatsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ConnStatsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ConnStatsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value serverId = const Value.absent(), + Value serverName = const Value.absent(), + Value timestamp = const Value.absent(), + Value result = const Value.absent(), + Value errorMessage = const Value.absent(), + Value durationMs = const Value.absent(), + }) => ConnStatsCompanion( + id: id, + serverId: serverId, + serverName: serverName, + timestamp: timestamp, + result: result, + errorMessage: errorMessage, + durationMs: durationMs, + ), + createCompanionCallback: + ({ + required String id, + required String serverId, + required String serverName, + required int timestamp, + required String result, + Value errorMessage = const Value.absent(), + required int durationMs, + }) => ConnStatsCompanion.insert( + id: id, + serverId: serverId, + serverName: serverName, + timestamp: timestamp, + result: result, + errorMessage: errorMessage, + durationMs: durationMs, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ConnStatsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({serverId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (serverId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.serverId, + referencedTable: $$ConnStatsTableReferences + ._serverIdTable(db), + referencedColumn: $$ConnStatsTableReferences + ._serverIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ConnStatsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $ConnStatsTable, + ConnStatRow, + $$ConnStatsTableFilterComposer, + $$ConnStatsTableOrderingComposer, + $$ConnStatsTableAnnotationComposer, + $$ConnStatsTableCreateCompanionBuilder, + $$ConnStatsTableUpdateCompanionBuilder, + (ConnStatRow, $$ConnStatsTableReferences), + ConnStatRow, + PrefetchHooks Function({bool serverId}) + >; +typedef $$AgentConversationsTableCreateCompanionBuilder = + AgentConversationsCompanion Function({ + required String id, + required String serverId, + required int updatedAt, + required String data, + }); +typedef $$AgentConversationsTableUpdateCompanionBuilder = + AgentConversationsCompanion Function({ + Value id, + Value serverId, + Value updatedAt, + Value data, + }); + +final class $$AgentConversationsTableReferences + extends + BaseReferences< + _$AppDb, + $AgentConversationsTable, + AgentConversationRow + > { + $$AgentConversationsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static MultiTypedResultKey< + $AgentActiveConversationsTable, + List + > + _agentActiveConversationsRefsTable( + _$AppDb db, + ) => MultiTypedResultKey.fromTable( + db.agentActiveConversations, + aliasName: + 'agent_conversation__id__agent_active_conversation__conversation_id', + ); + + $$AgentActiveConversationsTableProcessedTableManager + get agentActiveConversationsRefs { + final manager = $$AgentActiveConversationsTableTableManager( + $_db, + $_db.agentActiveConversations, + ).filter((f) => f.conversationId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _agentActiveConversationsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$AgentConversationsTableFilterComposer + extends Composer<_$AppDb, $AgentConversationsTable> { + $$AgentConversationsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnFilters(column), + ); + + Expression agentActiveConversationsRefs( + Expression Function($$AgentActiveConversationsTableFilterComposer f) + f, + ) { + final $$AgentActiveConversationsTableFilterComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.agentActiveConversations, + getReferencedColumn: (t) => t.conversationId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AgentActiveConversationsTableFilterComposer( + $db: $db, + $table: $db.agentActiveConversations, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$AgentConversationsTableOrderingComposer + extends Composer<_$AppDb, $AgentConversationsTable> { + $$AgentConversationsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$AgentConversationsTableAnnotationComposer + extends Composer<_$AppDb, $AgentConversationsTable> { + $$AgentConversationsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get data => + $composableBuilder(column: $table.data, builder: (column) => column); + + Expression agentActiveConversationsRefs( + Expression Function($$AgentActiveConversationsTableAnnotationComposer a) + f, + ) { + final $$AgentActiveConversationsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.agentActiveConversations, + getReferencedColumn: (t) => t.conversationId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AgentActiveConversationsTableAnnotationComposer( + $db: $db, + $table: $db.agentActiveConversations, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$AgentConversationsTableTableManager + extends + RootTableManager< + _$AppDb, + $AgentConversationsTable, + AgentConversationRow, + $$AgentConversationsTableFilterComposer, + $$AgentConversationsTableOrderingComposer, + $$AgentConversationsTableAnnotationComposer, + $$AgentConversationsTableCreateCompanionBuilder, + $$AgentConversationsTableUpdateCompanionBuilder, + (AgentConversationRow, $$AgentConversationsTableReferences), + AgentConversationRow, + PrefetchHooks Function({bool agentActiveConversationsRefs}) + > { + $$AgentConversationsTableTableManager( + _$AppDb db, + $AgentConversationsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$AgentConversationsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$AgentConversationsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$AgentConversationsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value serverId = const Value.absent(), + Value updatedAt = const Value.absent(), + Value data = const Value.absent(), + }) => AgentConversationsCompanion( + id: id, + serverId: serverId, + updatedAt: updatedAt, + data: data, + ), + createCompanionCallback: + ({ + required String id, + required String serverId, + required int updatedAt, + required String data, + }) => AgentConversationsCompanion.insert( + id: id, + serverId: serverId, + updatedAt: updatedAt, + data: data, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$AgentConversationsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({agentActiveConversationsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (agentActiveConversationsRefs) db.agentActiveConversations, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (agentActiveConversationsRefs) + await $_getPrefetchedData< + AgentConversationRow, + $AgentConversationsTable, + AgentActiveConversationRow + >( + currentTable: table, + referencedTable: $$AgentConversationsTableReferences + ._agentActiveConversationsRefsTable(db), + managerFromTypedResult: (p0) => + $$AgentConversationsTableReferences( + db, + table, + p0, + ).agentActiveConversationsRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where( + (e) => e.conversationId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$AgentConversationsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $AgentConversationsTable, + AgentConversationRow, + $$AgentConversationsTableFilterComposer, + $$AgentConversationsTableOrderingComposer, + $$AgentConversationsTableAnnotationComposer, + $$AgentConversationsTableCreateCompanionBuilder, + $$AgentConversationsTableUpdateCompanionBuilder, + (AgentConversationRow, $$AgentConversationsTableReferences), + AgentConversationRow, + PrefetchHooks Function({bool agentActiveConversationsRefs}) + >; +typedef $$AgentActiveConversationsTableCreateCompanionBuilder = + AgentActiveConversationsCompanion Function({ + required String serverId, + required String conversationId, + }); +typedef $$AgentActiveConversationsTableUpdateCompanionBuilder = + AgentActiveConversationsCompanion Function({ + Value serverId, + Value conversationId, + }); + +final class $$AgentActiveConversationsTableReferences + extends + BaseReferences< + _$AppDb, + $AgentActiveConversationsTable, + AgentActiveConversationRow + > { + $$AgentActiveConversationsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $AgentConversationsTable _conversationIdTable(_$AppDb db) => + db.agentConversations.createAlias( + 'agent_active_conversation__conversation_id__agent_conversation__id', + ); + + $$AgentConversationsTableProcessedTableManager get conversationId { + final $_column = $_itemColumn('conversation_id')!; + + final manager = $$AgentConversationsTableTableManager( + $_db, + $_db.agentConversations, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_conversationIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$AgentActiveConversationsTableFilterComposer + extends Composer<_$AppDb, $AgentActiveConversationsTable> { + $$AgentActiveConversationsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + $$AgentConversationsTableFilterComposer get conversationId { + final $$AgentConversationsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.conversationId, + referencedTable: $db.agentConversations, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AgentConversationsTableFilterComposer( + $db: $db, + $table: $db.agentConversations, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$AgentActiveConversationsTableOrderingComposer + extends Composer<_$AppDb, $AgentActiveConversationsTable> { + $$AgentActiveConversationsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + $$AgentConversationsTableOrderingComposer get conversationId { + final $$AgentConversationsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.conversationId, + referencedTable: $db.agentConversations, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AgentConversationsTableOrderingComposer( + $db: $db, + $table: $db.agentConversations, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$AgentActiveConversationsTableAnnotationComposer + extends Composer<_$AppDb, $AgentActiveConversationsTable> { + $$AgentActiveConversationsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + $$AgentConversationsTableAnnotationComposer get conversationId { + final $$AgentConversationsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.conversationId, + referencedTable: $db.agentConversations, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AgentConversationsTableAnnotationComposer( + $db: $db, + $table: $db.agentConversations, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$AgentActiveConversationsTableTableManager + extends + RootTableManager< + _$AppDb, + $AgentActiveConversationsTable, + AgentActiveConversationRow, + $$AgentActiveConversationsTableFilterComposer, + $$AgentActiveConversationsTableOrderingComposer, + $$AgentActiveConversationsTableAnnotationComposer, + $$AgentActiveConversationsTableCreateCompanionBuilder, + $$AgentActiveConversationsTableUpdateCompanionBuilder, + ( + AgentActiveConversationRow, + $$AgentActiveConversationsTableReferences, + ), + AgentActiveConversationRow, + PrefetchHooks Function({bool conversationId}) + > { + $$AgentActiveConversationsTableTableManager( + _$AppDb db, + $AgentActiveConversationsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$AgentActiveConversationsTableFilterComposer( + $db: db, + $table: table, + ), + createOrderingComposer: () => + $$AgentActiveConversationsTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$AgentActiveConversationsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value serverId = const Value.absent(), + Value conversationId = const Value.absent(), + }) => AgentActiveConversationsCompanion( + serverId: serverId, + conversationId: conversationId, + ), + createCompanionCallback: + ({required String serverId, required String conversationId}) => + AgentActiveConversationsCompanion.insert( + serverId: serverId, + conversationId: conversationId, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$AgentActiveConversationsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({conversationId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (conversationId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.conversationId, + referencedTable: + $$AgentActiveConversationsTableReferences + ._conversationIdTable(db), + referencedColumn: + $$AgentActiveConversationsTableReferences + ._conversationIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$AgentActiveConversationsTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $AgentActiveConversationsTable, + AgentActiveConversationRow, + $$AgentActiveConversationsTableFilterComposer, + $$AgentActiveConversationsTableOrderingComposer, + $$AgentActiveConversationsTableAnnotationComposer, + $$AgentActiveConversationsTableCreateCompanionBuilder, + $$AgentActiveConversationsTableUpdateCompanionBuilder, + (AgentActiveConversationRow, $$AgentActiveConversationsTableReferences), + AgentActiveConversationRow, + PrefetchHooks Function({bool conversationId}) + >; +typedef $$TombstonesTableCreateCompanionBuilder = + TombstonesCompanion Function({ + required String tbl, + required String rowId, + required int deletedAt, + }); +typedef $$TombstonesTableUpdateCompanionBuilder = + TombstonesCompanion Function({ + Value tbl, + Value rowId, + Value deletedAt, + }); + +class $$TombstonesTableFilterComposer + extends Composer<_$AppDb, $TombstonesTable> { + $$TombstonesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get tbl => $composableBuilder( + column: $table.tbl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get rowId => $composableBuilder( + column: $table.rowId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$TombstonesTableOrderingComposer + extends Composer<_$AppDb, $TombstonesTable> { + $$TombstonesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get tbl => $composableBuilder( + column: $table.tbl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get rowId => $composableBuilder( + column: $table.rowId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$TombstonesTableAnnotationComposer + extends Composer<_$AppDb, $TombstonesTable> { + $$TombstonesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get tbl => + $composableBuilder(column: $table.tbl, builder: (column) => column); + + GeneratedColumn get rowId => + $composableBuilder(column: $table.rowId, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); +} + +class $$TombstonesTableTableManager + extends + RootTableManager< + _$AppDb, + $TombstonesTable, + TombstoneRow, + $$TombstonesTableFilterComposer, + $$TombstonesTableOrderingComposer, + $$TombstonesTableAnnotationComposer, + $$TombstonesTableCreateCompanionBuilder, + $$TombstonesTableUpdateCompanionBuilder, + ( + TombstoneRow, + BaseReferences<_$AppDb, $TombstonesTable, TombstoneRow>, + ), + TombstoneRow, + PrefetchHooks Function() + > { + $$TombstonesTableTableManager(_$AppDb db, $TombstonesTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$TombstonesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$TombstonesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$TombstonesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value tbl = const Value.absent(), + Value rowId = const Value.absent(), + Value deletedAt = const Value.absent(), + }) => TombstonesCompanion( + tbl: tbl, + rowId: rowId, + deletedAt: deletedAt, + ), + createCompanionCallback: + ({ + required String tbl, + required String rowId, + required int deletedAt, + }) => TombstonesCompanion.insert( + tbl: tbl, + rowId: rowId, + deletedAt: deletedAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$TombstonesTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $TombstonesTable, + TombstoneRow, + $$TombstonesTableFilterComposer, + $$TombstonesTableOrderingComposer, + $$TombstonesTableAnnotationComposer, + $$TombstonesTableCreateCompanionBuilder, + $$TombstonesTableUpdateCompanionBuilder, + (TombstoneRow, BaseReferences<_$AppDb, $TombstonesTable, TombstoneRow>), + TombstoneRow, + PrefetchHooks Function() + >; +typedef $$SyncStatesTableCreateCompanionBuilder = + SyncStatesCompanion Function({required String k, required String v}); +typedef $$SyncStatesTableUpdateCompanionBuilder = + SyncStatesCompanion Function({Value k, Value v}); + +class $$SyncStatesTableFilterComposer + extends Composer<_$AppDb, $SyncStatesTable> { + $$SyncStatesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get k => $composableBuilder( + column: $table.k, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get v => $composableBuilder( + column: $table.v, + builder: (column) => ColumnFilters(column), + ); +} + +class $$SyncStatesTableOrderingComposer + extends Composer<_$AppDb, $SyncStatesTable> { + $$SyncStatesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get k => $composableBuilder( + column: $table.k, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get v => $composableBuilder( + column: $table.v, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$SyncStatesTableAnnotationComposer + extends Composer<_$AppDb, $SyncStatesTable> { + $$SyncStatesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get k => + $composableBuilder(column: $table.k, builder: (column) => column); + + GeneratedColumn get v => + $composableBuilder(column: $table.v, builder: (column) => column); +} + +class $$SyncStatesTableTableManager + extends + RootTableManager< + _$AppDb, + $SyncStatesTable, + SyncStateRow, + $$SyncStatesTableFilterComposer, + $$SyncStatesTableOrderingComposer, + $$SyncStatesTableAnnotationComposer, + $$SyncStatesTableCreateCompanionBuilder, + $$SyncStatesTableUpdateCompanionBuilder, + ( + SyncStateRow, + BaseReferences<_$AppDb, $SyncStatesTable, SyncStateRow>, + ), + SyncStateRow, + PrefetchHooks Function() + > { + $$SyncStatesTableTableManager(_$AppDb db, $SyncStatesTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SyncStatesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SyncStatesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SyncStatesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value k = const Value.absent(), + Value v = const Value.absent(), + }) => SyncStatesCompanion(k: k, v: v), + createCompanionCallback: ({required String k, required String v}) => + SyncStatesCompanion.insert(k: k, v: v), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$SyncStatesTableProcessedTableManager = + ProcessedTableManager< + _$AppDb, + $SyncStatesTable, + SyncStateRow, + $$SyncStatesTableFilterComposer, + $$SyncStatesTableOrderingComposer, + $$SyncStatesTableAnnotationComposer, + $$SyncStatesTableCreateCompanionBuilder, + $$SyncStatesTableUpdateCompanionBuilder, + (SyncStateRow, BaseReferences<_$AppDb, $SyncStatesTable, SyncStateRow>), + SyncStateRow, + PrefetchHooks Function() + >; + +class $AppDbManager { + final _$AppDb _db; + $AppDbManager(this._db); + $$PrivateKeysTableTableManager get privateKeys => + $$PrivateKeysTableTableManager(_db, _db.privateKeys); + $$ServersTableTableManager get servers => + $$ServersTableTableManager(_db, _db.servers); + $$ServerTagsTableTableManager get serverTags => + $$ServerTagsTableTableManager(_db, _db.serverTags); + $$ServerEnvsTableTableManager get serverEnvs => + $$ServerEnvsTableTableManager(_db, _db.serverEnvs); + $$ServerJumpsTableTableManager get serverJumps => + $$ServerJumpsTableTableManager(_db, _db.serverJumps); + $$ServerDisabledCmdsTableTableManager get serverDisabledCmds => + $$ServerDisabledCmdsTableTableManager(_db, _db.serverDisabledCmds); + $$ServerCustomCmdsTableTableManager get serverCustomCmds => + $$ServerCustomCmdsTableTableManager(_db, _db.serverCustomCmds); + $$KnownHostsTableTableManager get knownHosts => + $$KnownHostsTableTableManager(_db, _db.knownHosts); + $$SnippetsTableTableManager get snippets => + $$SnippetsTableTableManager(_db, _db.snippets); + $$SnippetTagsTableTableManager get snippetTags => + $$SnippetTagsTableTableManager(_db, _db.snippetTags); + $$SnippetAutoRunOnTableTableManager get snippetAutoRunOn => + $$SnippetAutoRunOnTableTableManager(_db, _db.snippetAutoRunOn); + $$PortForwardsTableTableManager get portForwards => + $$PortForwardsTableTableManager(_db, _db.portForwards); + $$ContainerHostsTableTableManager get containerHosts => + $$ContainerHostsTableTableManager(_db, _db.containerHosts); + $$ContainerRuntimesTableTableManager get containerRuntimes => + $$ContainerRuntimesTableTableManager(_db, _db.containerRuntimes); + $$ConnStatsTableTableManager get connStats => + $$ConnStatsTableTableManager(_db, _db.connStats); + $$AgentConversationsTableTableManager get agentConversations => + $$AgentConversationsTableTableManager(_db, _db.agentConversations); + $$AgentActiveConversationsTableTableManager get agentActiveConversations => + $$AgentActiveConversationsTableTableManager( + _db, + _db.agentActiveConversations, + ); + $$TombstonesTableTableManager get tombstones => + $$TombstonesTableTableManager(_db, _db.tombstones); + $$SyncStatesTableTableManager get syncStates => + $$SyncStatesTableTableManager(_db, _db.syncStates); +} diff --git a/lib/data/store/entity_store.dart b/lib/data/store/entity_store.dart new file mode 100644 index 0000000000..cfe0d0e809 --- /dev/null +++ b/lib/data/store/entity_store.dart @@ -0,0 +1,428 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:sqlite3/sqlite3.dart'; + +/// Raised when a write would collide with a `UNIQUE` column. +/// +/// Names are unique in the schema rather than in the two pages that can create +/// a record, so this is where a duplicate is found. The pages turn it into a +/// message; without it the failure is a raw `SqliteException` on the way out of +/// a button handler. +class DuplicateNameException implements Exception { + const DuplicateNameException(this.name); + + final String name; + + @override + String toString() => 'DuplicateNameException: $name'; +} + +/// One table that carries `updated_at` and `rev`, and the writes that keep +/// them true. +/// +/// Separate from [EntityStore] because a store is not always one table: the +/// container settings are two, keyed differently, and both have to be stamped +/// and tombstoned the same way. +class SyncedTable { + const SyncedTable(this.table, {this.idColumn = 'id'}); + + /// One of `Tables.syncRoots`. + final String table; + + /// The column a row is addressed by, and what a tombstone records. + final String idColumn; + + Database get db => SqliteDb.instance; + + /// Writes one row, leaving columns it does not name alone. + /// + /// Not `INSERT OR REPLACE`: that deletes the row and inserts a new one, so + /// every column absent from the statement goes back to its default. Here + /// that silently reset `rev` to 0 on every write, which is the one thing + /// `rev` exists to prevent — two edits in the same millisecond became + /// indistinguishable again. It would also fire every `ON DELETE CASCADE` + /// pointing at the row, taking the children with it. + void upsert( + List columns, + List values, { + List? keyColumns, + String? into, + }) { + final keys = keyColumns ?? [idColumn]; + final placeholders = List.filled(columns.length, '?').join(', '); + final assignments = [ + for (final c in columns) + if (!keys.contains(c)) '$c = excluded.$c', + ].join(', '); + db.execute( + 'INSERT INTO ${into ?? table} (${columns.join(', ')}) ' + 'VALUES ($placeholders) ' + 'ON CONFLICT (${keys.join(', ')}) DO UPDATE SET $assignments;', + values, + ); + } + + /// Marks the record changed, for the incremental sync to select on. + /// + /// `rev` moves too: two edits inside one millisecond are indistinguishable + /// by the clock alone. + void stamp(String id, {int? at}) { + db.execute( + 'UPDATE $table SET updated_at = ?, rev = rev + 1 WHERE $idColumn = ?;', + [at ?? DateTimeX.timestamp, id], + ); + db.execute('DELETE FROM tombstone WHERE tbl = ? AND row_id = ?;', [ + table, + id, + ]); + } + + /// Records that this record existed and no longer does. + /// + /// A peer that still holds the row would otherwise read its absence as an + /// addition and put it back on the next sync. + void tombstone(String id, {int? at}) { + db.execute( + 'INSERT OR REPLACE INTO tombstone (tbl, row_id, deleted_at) ' + 'VALUES (?, ?, ?);', + [table, id, at ?? DateTimeX.timestamp], + ); + } + + /// The newest change this table holds, including deletions. + /// + /// What `SqliteStore.lastUpdateTs` was for a K-V store, and read for the same + /// reason: sync compares it against the backup's to decide which side wins. A + /// delete counts — otherwise a device whose only change was removing a server + /// looks unchanged and gets the server pushed back to it. + int get lastModTime { + final rows = db.select( + 'SELECT MAX(t) AS t FROM (' + ' SELECT MAX(updated_at) AS t FROM $table' + ' UNION ALL' + ' SELECT MAX(deleted_at) FROM tombstone WHERE tbl = ?' + ');', + [table], + ); + return (rows.singleOrNull?['t'] as int?) ?? 0; + } +} + +/// A store whose records are rows in a table of their own. +/// +/// Not a [KvStore]: there is no key-addressed `get`/`set` here, because the +/// point of the move was that these records have columns, relations and +/// constraints. What it keeps is the shape the app already talks to — +/// [fetch], [fetchOneRaw], [put], [delete] — so a caller passing a model +/// around never learns which of the two backs it. +/// +/// Change notification replaces what `box.watch()` and `SqliteStore.watch()` +/// gave: SQLite has no change feed, so writes announce themselves. +abstract class EntityStore { + /// The table this store owns, one of `Tables.syncRoots`. + /// + /// A getter rather than a constructor argument: the name is fixed by the + /// schema Drift owns, so there is nothing for a caller to choose. + String get table; + + /// The column a row is addressed by. + String get idColumn => 'id'; + + late final SyncedTable synced = SyncedTable(table, idColumn: idColumn); + + Database get db => SqliteDb.instance; + + final _changes = StreamController.broadcast(); + List? _cache; + + /// Fires after any write through this store. + Stream watch() => _changes.stream; + + /// The id of [item], which is never something the user typed. + String idOf(T item); + + /// The user-typed name of [item], used only to describe a collision. + String? nameOf(T item) => null; + + /// [item] as a backup carries it. + Map toJson(T item); + + /// Rebuilds one record from what a backup carried. + /// + /// Returns null for anything unreadable — a restore skips that record rather + /// than failing over it. + T? fromJson(Map json); + + /// The incoming record, given the id it should be written under. + /// + /// The default is the id it arrived with. A store whose records were keyed by + /// name before ids existed overrides this: a backup from then carries no id, + /// `fromJson` generates a fresh one on every decode, and without matching the + /// name to what is already here, restoring the same file twice would try to + /// insert the record again — which the unique name refuses. + T reconcile(T incoming) => incoming; + + /// Reads every record. Implementations do this in as few statements as they + /// can: a query per row is what the key-value layout forced and this exists + /// to stop. + List readAll(); + + /// Every record, cached until the next write. + /// + /// A copy, so a caller sorting or filtering the result cannot reorder the + /// cache everyone else reads. The cache is dropped by the write methods + /// rather than by watching the database, because nothing can reach these + /// tables except through here. + List fetch() => List.from(_cache ??= readAll()); + + T? fetchOneRaw(String id) { + for (final item in _cache ??= readAll()) { + if (idOf(item) == id) return item; + } + return null; + } + + bool have(T item) => fetchOneRaw(idOf(item)) != null; + + List keys() => (_cache ??= readAll()).map(idOf).toList(); + + /// Writes [item], as one transaction with its child rows. + void put(T item) { + try { + SqliteStore.transact(() { + write(item); + writeLinks(item); + synced.stamp(idOf(item)); + }); + } on SqliteException catch (e) { + if (e.extendedResultCode == _uniqueViolation) { + throw DuplicateNameException(nameOf(item) ?? idOf(item)); + } + rethrow; + } + invalidate(); + } + + static const _uniqueViolation = 2067; // SQLITE_CONSTRAINT_UNIQUE + + /// Inserts or replaces the row and its children. Called inside a + /// transaction, with the timestamp applied afterwards. + void write(T item); + + /// Links [item] to other records of the same table. + /// + /// A second pass, because a foreign key pointing into this table cannot be + /// satisfied while the batch is still being written: a jump host is a + /// server, and the row it names may come later in the same restore. Writing + /// it inline dropped every forward reference. + void writeLinks(T item) {} + + void upsert( + List columns, + List values, { + List? keyColumns, + String? into, + }) => synced.upsert(columns, values, keyColumns: keyColumns, into: into); + + void update(T old, T neu) { + if (idOf(old) != idOf(neu)) { + // An id is not something the user edits, so this is a bug rather than a + // rename. Renaming what the user *did* type is an ordinary column write. + throw ArgumentError('cannot change the id of a $T'); + } + put(neu); + } + + void delete(T item) => deleteById(idOf(item)); + + void deleteById(String id) { + SqliteStore.transact(() { + // Children go with it: every child table declares ON DELETE CASCADE, so + // this is one statement rather than the six the old code hand-wrote — + // and the four it forgot. + db.execute('DELETE FROM $table WHERE $idColumn = ?;', [id]); + synced.tombstone(id); + }); + invalidate(); + } + + /// `Future` to match `SqliteStore.clear`, which callers guard on: a + /// store is a store at the call site, whichever shape backs it. Failure is a + /// throw, so this only ever answers true. + Future clear() async { + SqliteStore.transact(() { + for (final id in keys()) { + synced.tombstone(id); + } + db.execute('DELETE FROM $table;'); + }); + invalidate(); + return true; + } + + /// Bumps the parent after a child row changed, so an edit to a tag or an env + /// is a change to the server that owns it. + void touch(String id) { + SqliteStore.transact(() => synced.stamp(id)); + invalidate(); + } + + int get lastModTime => synced.lastModTime; + + /// When each record last changed, deletions included. + /// + /// A tombstone's id is in here with the time it was deleted and *not* in + /// [getAllMap], which is what tells a merging peer the record was removed + /// rather than never seen. + Map get timestamps => { + for (final row in db.select( + 'SELECT $idColumn AS id, updated_at FROM $table;', + )) + row['id'] as String: row['updated_at'] as int? ?? 0, + for (final row in db.select( + 'SELECT row_id, deleted_at FROM tombstone WHERE tbl = ?;', + [table], + )) + row['row_id'] as String: row['deleted_at'] as int? ?? 0, + }; + + /// Everything this store holds, in the shape a backup carries. + /// + /// The timestamps ride along under the same key a K-V store uses, so one + /// envelope describes both kinds of store and [merge] can be the same + /// last-write-wins rule on either side. + Map getAllMap() => { + for (final item in fetch()) idOf(item): toJson(item), + lastModKey: timestamps, + }; + + /// Matches `KvStore.lastUpdateTsKey`, and starts with the internal prefix so + /// nothing mistakes it for a record. + static const lastModKey = StoreDefaults.defaultLastUpdateTsKey; + + /// Folds a backup's records into this store, newest write winning per record. + /// + /// [force] takes the backup's version of every record regardless of when + /// either side changed. Otherwise a record moves only if the backup's + /// timestamp is the later one — which is also how a delete travels: the id is + /// in the backup's timestamps and not among its records. + /// + /// Returns whether anything changed, which is what tells a provider to + /// reload. + bool merge(Map backupData, {required bool force}) { + final incoming = _timestampsOf(backupData[lastModKey]); + final current = timestamps; + final records = backupData.keys + .where((k) => !k.startsWith(StoreDefaults.prefixKey)) + .toSet(); + + var changed = false; + SqliteStore.transact(() { + final written = []; + for (final id in {...records, ...current.keys}) { + final bakTs = incoming[id]; + final known = current.containsKey(id); + // A record this device has never seen is an addition. Comparing + // timestamps first made `0 <= 0` a tie and dropped every record from a + // backup that carries none — which is every older envelope. + if (!force && known && (bakTs ?? 0) <= (current[id] ?? 0)) continue; + // And a record with no timestamp is stamped as now. Stamping 0 would + // leave it looking older than anything, and the next sync would take + // it straight back out. + final at = bakTs ?? DateTimeX.timestamp; + + if (!records.contains(id)) { + // Only a record the backup knew about and no longer holds is a + // delete; one it never had a timestamp for says nothing. + if (!known || bakTs == null) continue; + db.execute('DELETE FROM $table WHERE $idColumn = ?;', [id]); + synced.tombstone(id, at: at); + changed = true; + continue; + } + + final raw = backupData[id]; + if (raw is! Map) continue; + final item = fromJson(Map.from(raw)); + if (item == null) continue; + try { + final resolved = reconcile(item); + write(resolved); + written.add(resolved); + synced.stamp(idOf(resolved), at: at); + changed = true; + } on SqliteException catch (e) { + // One record that cannot be written must not fail the restore: a + // backup can name a server this device deleted, and refusing the + // whole file over it would leave the user with neither copy. + Loggers.app.warning('Restore skipped $table/$id', e); + } + } + // Once every row exists, so a reference to one written later still lands. + for (final item in written) { + writeLinks(item); + } + }); + if (changed) invalidate(); + return changed; + } + + /// Replaces everything with [items], for the v1 backup format, which carries + /// no per-record timestamps and so can only be taken or left whole. + bool replaceAll(Iterable items) { + SqliteStore.transact(() { + for (final id in keys()) { + synced.tombstone(id); + } + db.execute('DELETE FROM $table;'); + final written = []; + for (final item in items) { + try { + final resolved = reconcile(item); + write(resolved); + written.add(resolved); + synced.stamp(idOf(resolved)); + } on SqliteException catch (e) { + Loggers.app.warning('Restore skipped a $T', e); + } + } + // Once every row exists — see [writeLinks]. + for (final item in written) { + writeLinks(item); + } + }); + invalidate(); + return true; + } + + static Map _timestampsOf(Object? raw) { + final decoded = raw is String ? _tryDecode(raw) : raw; + if (decoded is! Map) return const {}; + return { + for (final entry in decoded.entries) + if (entry.value is int) '${entry.key}': entry.value as int, + }; + } + + static Object? _tryDecode(String raw) { + try { + return json.decode(raw); + } catch (_) { + return null; + } + } + + /// Drops the cache without announcing anything. + /// + /// For a reader that wants to re-read — a provider reloading after a + /// restore. [invalidate] is for a writer, and telling every listener to + /// re-read because one of them chose to would be a loop. + void dropCache() => _cache = null; + + void invalidate() { + _cache = null; + if (!_changes.isClosed) _changes.add(null); + } +} diff --git a/lib/data/store/migrations/m003_hive_to_sqlite.dart b/lib/data/store/migrations/m003_hive_to_sqlite.dart index c4859a4f13..79094e4393 100644 --- a/lib/data/store/migrations/m003_hive_to_sqlite.dart +++ b/lib/data/store/migrations/m003_hive_to_sqlite.dart @@ -1,8 +1,10 @@ +import 'dart:convert'; import 'dart:io'; import 'package:fl_lib/fl_lib.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/schema.dart'; +import 'package:server_box/hive/legacy_adapters.dart'; import 'package:server_box/hive/spi_legacy_adapter.dart'; /// Copies every Hive box into the SQLite stores, once per device. @@ -14,8 +16,8 @@ import 'package:server_box/hive/spi_legacy_adapter.dart'; /// /// TODO: delete this, `lib/hive/`, the `hive_ce*` dependencies and the /// `Hive.initFlutter()` call in `main.dart` once no supported install can still -/// be on Hive. Keep [SpiLegacyAdapter] until then — reading a pre-v3 record is -/// what [_toSpi] needs it for. +/// be on Hive. Keep the frozen adapters until then — reading what a released +/// build wrote is what [_fromLegacy] needs them for. abstract final class HiveImport { /// Internal, so it stays out of backups and out of `lastUpdateTs`. static const _markerKey = '${StoreDefaults.prefixKey}hiveImported'; @@ -27,22 +29,28 @@ abstract final class HiveImport { /// Box name -> what takes one of its rows. /// - /// Most go to a K-V store under the same key. The last two own tables now, so - /// they take the row apart themselves. + /// Every box lands in `kv` under its own store name, one row per Hive key. + /// That is the whole of the v4 shape, and it is all this step knows how to + /// produce: taking a record apart into columns is m004's job, against data + /// that is already off Hive. + /// + /// Straight into `kv` rather than through the store objects, because those + /// stores have moved on to tables — a migration that calls today's code + /// changes meaning every time that code does. /// /// `conn_stats_index` is deliberately absent: it held nothing that is not /// derivable from the records, and it is the one box that was never /// encrypted — so it is deleted below rather than carried across. static Map get _boxes => { 'setting': _intoKv(Stores.setting), - 'server': _intoKv(Stores.server), - 'docker': _intoKv(Stores.container), - 'key': _intoKv(Stores.key), - 'snippet': _intoKv(Stores.snippet), 'history': _intoKv(Stores.history), - 'port_forward': _intoKv(Stores.portForward), - 'connection_stats': Stores.connectionStats.importRow, - 'agent_conversation': Stores.agentConversation.importRow, + 'server': _intoKvTable('server'), + 'docker': _intoKvTable('docker'), + 'key': _intoKvTable('key'), + 'snippet': _intoKvTable('snippet'), + 'port_forward': _intoKvTable('port_forward'), + 'connection_stats': _intoKvTable('conn_stat'), + 'agent_conversation': _intoKvTable('agent_conversation'), }; /// `updateLastUpdateTsOnSet: false`: the timestamps are copied across with @@ -52,6 +60,26 @@ abstract final class HiveImport { static bool Function(String, Object) _intoKv(SqliteStore store) => (key, value) => store.set(key, value, updateLastUpdateTsOnSet: false); + /// Writes a row of the v4 key-value layout directly. + /// + /// `updated_at` is 0, not now: m004 carries these timestamps into the entity + /// tables, and stamping them as today would make the first sync after + /// upgrading read as "everything changed". + static bool Function(String, Object) _intoKvTable(String store) => + (key, value) { + try { + SqliteDb.instance.execute( + 'INSERT OR REPLACE INTO kv (store, key, value, updated_at) ' + 'VALUES (?, ?, ?, 0);', + [store, key, json.encode(value)], + ); + return true; + } catch (e) { + Loggers.app.warning('m003: could not write $store/$key', e); + return false; + } + }; + /// Runs the import if this device has data in Hive and none in SQLite yet. /// /// Safe to re-run: nothing is deleted from Hive, so a crash part-way through @@ -121,7 +149,7 @@ abstract final class HiveImport { // What did land is already in the current shape, so the version is set // now: a launch in this state runs the migrator like any other, and the // step for a shape this data no longer has must not be applied to it. - if (done.isNotEmpty) SchemaVersion.initFresh(); + if (done.isNotEmpty) SchemaVersion.initAtHiveImport(); return; } @@ -129,9 +157,10 @@ abstract final class HiveImport { _dropPlaintextIndex(dir); // The copy nests a pre-v3 server record on the way across, which is what - // the v2 -> v3 step used to do in place, so what has landed is current by - // construction. - SchemaVersion.initFresh(); + // the v2 -> v3 step used to do in place. What has landed is the layout of + // the build that dropped Hive, not the current one — the steps after this + // still have to run over it. + SchemaVersion.initAtHiveImport(); Stores.setting.set(_markerKey, true); Stores.setting.remove(_doneKey); } @@ -176,7 +205,7 @@ abstract final class HiveImport { final raw = legacy.box.get(key); if (raw == null) continue; - final value = _toSpi(raw) ?? raw; + final value = _fromLegacy(raw) ?? raw; final ok = into(key, _jsonSafe(value as Object)); if (ok) { copied++; @@ -196,19 +225,22 @@ abstract final class HiveImport { return (opened: true, copied: copied); } - /// A pre-v3 server record, nested into the current shape. + /// A record that a released build wrote, as the JSON that build produced. /// - /// Returns null for anything else, including a record already in the current - /// shape — those encode themselves. - static Object? _toSpi(Object raw) => - raw is LegacySpiV2 ? raw.toSpi() : null; + /// Returns null for anything else, including a record whose adapter is still + /// generated from the live model — those encode themselves. + static Object? _fromLegacy(Object raw) => switch (raw) { + final LegacySpiV2 spi => spi.toSpi(), + final LegacyPrivateKeyV1 key => key.toJson(), + final LegacySnippetV1 snippet => snippet.toJson(), + _ => null, + }; /// The value as something made of maps, lists and primitives. /// /// A Hive box hands back whatever its adapter decoded — a `ConnectionStat`, - /// not a map. The K-V stores would encode that on write, but the two - /// table-backed stores parse what they are given with `fromJson`, so both - /// kinds of destination are handed the same shape. + /// not a map — and every destination here is a JSON column, so the value has + /// to be reduced to maps, lists and primitives before it can be encoded. static Object _jsonSafe(Object value) { if (value is Map || value is List || value is Enum) return value; if (value is num || value is String || value is bool) return value; diff --git a/lib/data/store/migrations/m004_kv_to_tables.dart b/lib/data/store/migrations/m004_kv_to_tables.dart new file mode 100644 index 0000000000..ef3e0d3f3f --- /dev/null +++ b/lib/data/store/migrations/m004_kv_to_tables.dart @@ -0,0 +1,657 @@ +import 'dart:convert'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:server_box/data/store/schema.dart'; +import 'package:sqlite3/sqlite3.dart'; + +/// Moves the entities out of `kv` and into tables with columns. +/// +/// Everything below is one transaction. A migration that stops half way is +/// worse than one that has not started: the records exist in two shapes at +/// once and nothing can tell which is authoritative. +/// +/// The tables themselves are not created here — the schema is created when the +/// database is opened, so this step only ever reads `kv` and inserts. That is +/// also why it can stay inside one synchronous transaction: creating the schema +/// means opening Drift over the connection, which is asynchronous, and a +/// synchronous transaction block cannot await it. +/// +/// Four things it does beyond copying: +/// +/// - **Gives snippets and private keys real ids.** Both were keyed by a name +/// the user typed — a private key's `id` *was* its name — so `Spi.ssh.keyId` +/// pointed at a name, and renaming a key silently detached every server +/// using it. New ids are generated here and the references rewritten. +/// - **Gives a server with no id one.** A record from before 1155 was stored +/// under `user@ip:port` with an empty `id` field, which the app fixed up at +/// every launch afterwards. There is nowhere to keep an empty primary key, +/// so it happens here instead, once, with every reference rewritten. +/// - **Recovers an `IdentityFile` path out of `keyId`.** `~/.ssh/config` import +/// used to write a path into the field that names a private key. Mapping the +/// old key ids would turn that into a null and lose it, so a value naming no +/// key that looks like a path lands in `ssh_key_path`, which is what it meant. +/// - **Drops rows that point at nothing.** Every child table has a foreign key +/// now, and the old `delServer` cleaned up by hand and missed cases, so an +/// upgrading install has statistics and container hosts for servers deleted +/// long ago. They cannot be inserted and are not worth keeping. +class KvToTablesMigration implements SchemaMigration { + const KvToTablesMigration(); + + @override + int get from => 4; + + Database get _db => SqliteDb.instance; + + /// The `kv` stores this consumes. Left behind they would be a second copy + /// that nothing reads and a backup would still carry. + static const _consumed = [ + 'server', + 'key', + 'snippet', + 'port_forward', + 'docker', + 'conn_stat', + 'agent_conversation', + ]; + + @override + Future apply() async { + SqliteStore.transact(() { + final keyIds = _migratePrivateKeys(); + final serverIds = _migrateServers(keyIds); + _migrateKnownHosts(serverIds); + final snippetNames = _migrateSnippets(serverIds); + _migratePortForwards(serverIds); + _migrateContainer(serverIds); + _migrateConnStats(serverIds); + _migrateAgentConversations(serverIds); + _rewriteOrder('serverOrder', (entry) => serverIds[entry]); + // Occurrence by occurrence: two snippets could share a stored name, and + // only one of them keeps it. A plain map would send both order entries + // to whichever was de-duplicated last. + _rewriteOrder('snippetOrder', (entry) { + final queue = snippetNames[entry]; + return queue == null || queue.isEmpty ? null : queue.removeAt(0); + }); + + for (final store in _consumed) { + _db.execute('DELETE FROM kv WHERE store = ?;', [store]); + } + _db.execute("DELETE FROM kv WHERE store = 'setting' AND key = ?;", [ + 'sshKnownHostFingerprints', + ]); + }); + } + + /// Every row of one `kv` store, decoded, with the timestamp it was written. + /// + /// `updated_at` is carried across rather than stamped as now: an incremental + /// sync reads it, and calling every record "changed today" would make the + /// first sync after upgrading upload everything. + List<({String key, Map value, int updatedAt})> _rows( + String store, + ) { + final out = <({String key, Map value, int updatedAt})>[]; + for (final row in _raw(store)) { + if (row.value is! Map) continue; + out.add(( + key: row.key, + value: Map.from(row.value as Map), + updatedAt: row.updatedAt, + )); + } + return out; + } + + /// The same, without requiring the value to be an object: the agent box held + /// a bare string under half of its keys. + List<({String key, Object? value, int updatedAt})> _raw(String store) { + final out = <({String key, Object? value, int updatedAt})>[]; + for (final row in _db.select( + 'SELECT key, value, updated_at FROM kv WHERE store = ?;', + [store], + )) { + final key = row['key'] as String; + if (key.startsWith(StoreDefaults.prefixKey)) continue; + try { + out.add(( + key: key, + value: json.decode(row['value'] as String), + updatedAt: row['updated_at'] as int? ?? 0, + )); + } catch (e) { + Loggers.app.warning('m004: skipping unreadable $store/$key', e); + } + } + return out; + } + + static int _bool(Object? v, {bool fallback = false}) => + (v is bool ? v : fallback) ? 1 : 0; + + /// Old name-as-id -> new generated id. + Map _migratePrivateKeys() { + final ids = {}; + final names = {}; + for (final row in _rows('key')) { + final oldId = row.value['id'] as String? ?? row.key; + // `private_key` is what the released model's `toJson` called it. + final key = (row.value['private_key'] ?? row.value['key']) as String?; + if (key == null) continue; + + // The old key was the name, and nothing enforced that it was unique + // across the two places one could be created. + var name = oldId; + for (var n = 2; !names.add(name); n++) { + name = '$oldId ($n)'; + } + + final id = ShortId.generate(); + ids[oldId] = id; + _db.execute( + 'INSERT INTO private_key (id, name, key, updated_at) ' + 'VALUES (?, ?, ?, ?);', + [id, name, key, row.updatedAt], + ); + } + return ids; + } + + /// Every way a server could be referred to before -> the id it has now. + /// + /// Two entries per server where the two differ: the `kv` key it was stored + /// under, and the `id` field inside the record. They are the same for + /// everything written since 1155, and for anything older the field is empty + /// and the key is `user@ip:port` — which other records point at. + Map _migrateServers(Map keyIds) { + final ids = {}; + final jumps = >{}; + + for (final row in _rows('server')) { + final v = row.value; + final stored = v['id'] as String?; + final id = stored != null && stored.isNotEmpty + ? stored + : ShortId.generate(); + final ssh = v['ssh'] as Map?; + final monitor = v['monitorHttp'] as Map?; + final sshIp = ssh?['ip'] as String?; + final monitorAddr = monitor?['addr'] as String?; + + // The schema now says a server is reached one way or the other. A record + // with neither was already unusable — `genClient` had nothing to dial — + // and one with both was ambiguous. Neither can be represented, so say so + // rather than failing the whole migration. + final hasSsh = sshIp != null && sshIp.isNotEmpty; + final hasMonitor = monitorAddr != null && monitorAddr.isNotEmpty; + if (hasSsh == hasMonitor) { + Loggers.app.warning( + 'm004: server "$id" has ${hasSsh ? 'both' : 'neither'} SSH and ' + 'monitor; it could not be connected to and is dropped', + ); + continue; + } + + final custom = v['custom'] as Map? ?? const {}; + final wol = v['wolCfg'] as Map?; + + // A key id that names no key. If it looks like a path it is one: the + // `~/.ssh/config` import wrote `IdentityFile` into this field. Mapping it + // to null would be the only record that it ever existed. + // `pubKeyId` is what `SshCredential.toJson` calls it, kept from the + // flat pre-v3 layout. Reading `keyId` alone found nothing and detached + // every server from its key. + final oldKeyId = (ssh?['pubKeyId'] ?? ssh?['keyId']) as String?; + final newKeyId = keyIds[oldKeyId]; + final keyPath = newKeyId == null && oldKeyId != null && _isPath(oldKeyId) + ? oldKeyId + : ssh?['keyPath'] as String?; + + _db.execute( + 'INSERT INTO server (' + 'id, name, auto_connect, system_type, ' + 'ssh_ip, ssh_port, ssh_user, ssh_pwd, ssh_key_id, ssh_key_path, ' + 'ssh_alter_url, ssh_proxy_command, ' + 'monitor_addr, monitor_user, monitor_pwd, monitor_ignore_cert, ' + 'wol_mac, wol_ip, wol_pwd, ' + 'pve_addr, pve_ignore_cert, pve_pwd, prefer_temp_dev, ' + 'temp_is_celsius, logo_url, net_dev, script_dir, updated_at' + ') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ' + '?, ?, ?, ?, ?, ?, ?, ?, ?);', + [ + id, + v['name'] as String? ?? id, + _bool(v['autoConnect'], fallback: true), + v['customSystemType'] as String?, + hasSsh ? sshIp : null, + hasSsh ? ((ssh?['port'] as num?)?.toInt() ?? 22) : null, + hasSsh ? ((ssh?['user'] as String?) ?? 'root') : null, + ssh?['pwd'] as String?, + // Points at the new id, so renaming the key later cannot detach it. + newKeyId, + keyPath, + ssh?['alterUrl'] as String?, + ssh?['proxyCommand'] as String?, + hasMonitor ? monitorAddr : null, + monitor?['user'] as String?, + monitor?['pwd'] as String?, + hasMonitor ? _bool(monitor?['ignoreCert']) : null, + wol?['mac'] as String?, + wol?['ip'] as String?, + wol?['pwd'] as String?, + custom['pveAddr'] as String?, + _bool(custom['pveIgnoreCert']), + custom['pvePwd'] as String?, + custom['preferTempDev'] as String?, + _bool(custom['tempIsCelsius'], fallback: true), + custom['logoUrl'] as String?, + custom['netDev'] as String?, + custom['scriptDir'] as String?, + row.updatedAt, + ], + ); + ids[row.key] = id; + if (stored != null && stored.isNotEmpty) ids[stored] = id; + + for (final tag in (v['tags'] as List? ?? const []).whereType()) { + _db.execute('INSERT OR IGNORE INTO server_tag VALUES (?, ?);', [ + id, + tag, + ]); + } + (v['envs'] as Map? ?? const {}).forEach((k, val) { + _db.execute('INSERT OR IGNORE INTO server_env VALUES (?, ?, ?);', [ + id, + '$k', + '$val', + ]); + }); + for (final cmd + in (v['disabledCmdTypes'] as List? ?? const []).whereType()) { + _db.execute('INSERT OR IGNORE INTO server_disabled_cmd VALUES (?, ?);', [ + id, + cmd, + ]); + } + (custom['cmds'] as Map? ?? const {}).forEach((k, val) { + _db.execute('INSERT OR IGNORE INTO server_custom_cmd VALUES (?, ?, ?);', [ + id, + '$k', + '$val', + ]); + }); + + // Held back: a jump host is a server, so the row it points at may not + // have been inserted yet — and its id may not be known yet either. + final ordered = [ + for (final j in [ssh?['jumpId'], ...?(ssh?['jumpIds'] as List?)]) + if (j is String && j.isNotEmpty) j, + ]; + final seen = {}; + final deduped = [ + for (final j in ordered) + if (seen.add(j)) j, + ]; + if (deduped.isNotEmpty) jumps[id] = deduped; + } + + jumps.forEach((serverId, targets) { + var ord = 0; + for (final target in targets) { + // A jump host that no longer exists is a dead reference the old shape + // could hold and this one cannot. + final resolved = ids[target]; + if (resolved == null) { + Loggers.app.warning( + 'm004: server "$serverId" jumps via "$target", which does not ' + 'exist; dropped', + ); + continue; + } + _db.execute('INSERT INTO server_jump VALUES (?, ?, ?);', [ + serverId, + ord++, + resolved, + ]); + } + }); + + return ids; + } + + /// Whether [value] is a filesystem path rather than a key id. + /// + /// `ShortId`'s alphabet is `0-9a-zA-Z-+`, so a generated id can match none of + /// these; a user-typed key name can, which is why the caller checks that it + /// names no key first. + /// + /// Known gap: `IdentityFile id_ed25519` with no separator at all is legal in + /// an ssh config, and such a value is indistinguishable from a key the user + /// has since deleted. It stays a dangling reference, which the edit page + /// shows as "no key" — the same as before. + static bool _isPath(String value) => + value.startsWith('~') || value.contains('/') || value.contains(r'\'); + + /// `sshKnownHostFingerprints`, a JSON map in `setting` keyed + /// `::`, becomes rows that cascade with their server. + void _migrateKnownHosts(Map serverIds) { + final raw = _db.select( + "SELECT value FROM kv WHERE store = 'setting' AND key = ?;", + ['sshKnownHostFingerprints'], + ); + if (raw.isEmpty) return; + Object? decoded; + try { + decoded = json.decode(raw.single['value'] as String); + } catch (e) { + Loggers.app.warning('m004: known hosts unreadable', e); + return; + } + if (decoded is! Map) return; + + decoded.forEach((k, v) { + // Split once from the left: a key type never contains `::`, and an old + // `user@ip:port` server id does contain colons. + final at = '$k'.indexOf('::'); + if (at <= 0) return; + final serverId = serverIds['$k'.substring(0, at)]; + if (serverId == null) return; + _db.execute('INSERT OR IGNORE INTO known_host VALUES (?, ?, ?);', [ + serverId, + '$k'.substring(at + 2), + '$v', + ]); + }); + } + + /// Old name -> the names its records ended up with, in the order they were + /// migrated. `snippetOrder` is a list of names, and a name that was not + /// unique produced more than one. + Map> _migrateSnippets(Map serverIds) { + final renamed = >{}; + final names = {}; + for (final row in _rows('snippet')) { + final v = row.value; + final script = v['script'] as String?; + if (script == null) continue; + + final oldName = v['name'] as String? ?? row.key; + var name = oldName; + for (var n = 2; !names.add(name); n++) { + name = '$oldName ($n)'; + } + (renamed[oldName] ??= []).add(name); + + final id = ShortId.generate(); + _db.execute( + 'INSERT INTO snippet (id, name, script, note, updated_at) ' + 'VALUES (?, ?, ?, ?, ?);', + [id, name, script, v['note'] as String?, row.updatedAt], + ); + for (final tag in (v['tags'] as List? ?? const []).whereType()) { + _db.execute('INSERT OR IGNORE INTO snippet_tag VALUES (?, ?);', [ + id, + tag, + ]); + } + for (final target + in (v['autoRunOn'] as List? ?? const []).whereType()) { + final serverId = serverIds[target]; + if (serverId == null) continue; + _db.execute('INSERT OR IGNORE INTO snippet_auto_run_on VALUES (?, ?);', [ + id, + serverId, + ]); + } + } + return renamed; + } + + void _migratePortForwards(Map serverIds) { + for (final row in _rows('port_forward')) { + final v = row.value; + final id = v['id'] as String? ?? row.key; + final serverId = serverIds[v['serverId'] as String?]; + if (serverId == null) { + Loggers.app.warning( + 'm004: port forward "$id" names server "${v['serverId']}", which ' + 'does not exist; dropped', + ); + continue; + } + const types = {'local', 'remote', 'dynamic'}; + final type = v['type'] as String?; + _db.execute( + 'INSERT INTO port_forward (id, server_id, name, type, local_host, ' + 'local_port, remote_host, remote_port, updated_at) ' + 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);', + [ + id, + serverId, + v['name'] as String? ?? id, + types.contains(type) ? type : 'local', + v['localHost'] as String?, + (v['localPort'] as num?)?.toInt() ?? 0, + v['remoteHost'] as String?, + (v['remotePort'] as num?)?.toInt(), + row.updatedAt, + ], + ); + } + } + + /// The `docker` store held three kinds of key under one namespace: + /// + /// - `containerHost` -> the host for that runtime + /// - `providerConfig` -> `ContainerType.podman`, from + /// `toString()` on the enum + /// - `` -> the Docker host, from before there + /// was a runtime to name + void _migrateContainer(Map serverIds) { + const hostPrefix = 'containerHost'; + const typePrefix = 'providerConfig'; + const runtimes = ['docker', 'podman']; + + for (final row in _raw('docker')) { + final value = row.value; + if (value is! String || value.isEmpty) continue; + + if (row.key.startsWith(hostPrefix)) { + final rest = row.key.substring(hostPrefix.length); + final type = runtimes.firstWhereOrNull(rest.startsWith); + if (type == null) continue; + final serverId = serverIds[rest.substring(type.length)]; + if (serverId == null) continue; + _db.execute( + 'INSERT OR REPLACE INTO container_host (server_id, type, host) ' + 'VALUES (?, ?, ?);', + [serverId, type, value], + ); + continue; + } + + if (row.key.startsWith(typePrefix)) { + final serverId = serverIds[row.key.substring(typePrefix.length)]; + // `providerConfig` with no id was a global override on top of + // `usePodman`, which says the same thing; it belongs to no server and + // there is nowhere to put it. + if (serverId == null) continue; + final type = runtimes.firstWhereOrNull(value.endsWith); + if (type == null) continue; + _db.execute( + 'INSERT OR REPLACE INTO container_runtime (server_id, type) ' + 'VALUES (?, ?);', + [serverId, type], + ); + continue; + } + + // Bare server id: the Docker host as it was stored before per-runtime + // hosts existed. `fetch` still fell back to it, so dropping it would + // silently move those servers back to the local socket. + final serverId = serverIds[row.key]; + if (serverId == null) continue; + _db.execute( + 'INSERT OR IGNORE INTO container_host (server_id, type, host) ' + 'VALUES (?, ?, ?);', + [serverId, 'docker', value], + ); + } + } + + /// `ConnectionResult` as JSON -> as stored. + /// + /// The model's `@JsonValue`s are snake_case and the column holds the enum's + /// `name`, which is not the same string for three of the five. Written out + /// rather than read off the enum: this has to keep meaning what it meant when + /// the data was written, whatever the model does later. + static const _results = { + 'success': 'success', + 'timeout': 'timeout', + 'auth_failed': 'authFailed', + 'network_error': 'networkError', + 'unknown_error': 'unknownError', + }; + + void _migrateConnStats(Map serverIds) { + var dropped = 0; + for (final row in _rows('conn_stat')) { + final v = row.value; + final serverId = serverIds[v['serverId'] as String?]; + if (serverId == null) { + dropped++; + continue; + } + final timestamp = DateTime.tryParse('${v['timestamp']}'); + if (timestamp == null) continue; + // A fresh id: the old one was `_`, so two attempts in + // the same millisecond shared a key and the second overwrote the first. + _db.execute( + 'INSERT INTO conn_stat (id, server_id, server_name, timestamp, ' + 'result, error_message, duration_ms) VALUES (?, ?, ?, ?, ?, ?, ?);', + [ + ShortId.generate(), + serverId, + v['serverName'] as String? ?? '', + timestamp.millisecondsSinceEpoch, + _results['${v['result']}'] ?? 'unknownError', + v['errorMessage'] as String? ?? '', + (v['durationMs'] as num?)?.toInt() ?? 0, + ], + ); + } + if (dropped > 0) { + Loggers.app.info('m004: dropped $dropped stats for deleted servers'); + } + } + + /// The agent box held conversations and the per-server active one under one + /// namespace, told apart by a key prefix. + /// + /// A scope that names a server follows that server's id, which this + /// migration may have regenerated. One that names no server is kept as it + /// is — the global agent's scope is not a server, which is also why the + /// table has no foreign key. + void _migrateAgentConversations(Map serverIds) { + const conversationPrefix = 'conversation::'; + const activePrefix = 'active::'; + final active = {}; + + for (final row in _raw('agent_conversation')) { + if (row.key.startsWith(activePrefix)) { + final id = row.value; + if (id is String && id.isNotEmpty) { + final scope = row.key.substring(activePrefix.length); + active[serverIds[scope] ?? scope] = id; + } + continue; + } + if (!row.key.startsWith(conversationPrefix)) continue; + final v = row.value; + if (v is! Map) continue; + // snake_case: `AgentConversation.toJson` is hand-written and that is + // what it produces, so reading `serverId` here found nothing and every + // conversation was dropped. + final id = v['id'] as String?; + final serverId = v['server_id'] as String?; + if (id == null || id.isEmpty || serverId == null || serverId.isEmpty) { + continue; + } + final updatedAt = DateTime.tryParse('${v['updated_at']}'); + final scope = serverIds[serverId] ?? serverId; + // Inside the payload too, not only in the column. The store rebuilds a + // conversation from `data` and then compares `conversation.serverId` + // against the server it was asked about — `fetchActive`, `setActive` and + // `deleteConversation` all do — so a record whose JSON still named the + // old id would be unreachable by every one of them. + v['server_id'] = scope; + // `ON CONFLICT`, not `OR REPLACE`: the active row references this one and + // cascades, so replacing would delete the record of which conversation + // is open. + _db.execute( + 'INSERT INTO agent_conversation (id, server_id, updated_at, data) ' + 'VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET ' + 'server_id = excluded.server_id, updated_at = excluded.updated_at, ' + 'data = excluded.data;', + [ + id, + scope, + updatedAt?.millisecondsSinceEpoch ?? row.updatedAt, + json.encode(v), + ], + ); + } + + active.forEach((serverId, conversationId) { + // The active row references a real conversation now. + final exists = _db + .select('SELECT 1 FROM agent_conversation WHERE id = ?;', [ + conversationId, + ]) + .isNotEmpty; + if (!exists) return; + _db.execute( + 'INSERT INTO agent_active_conversation (server_id, conversation_id) ' + 'VALUES (?, ?) ON CONFLICT (server_id) DO UPDATE SET ' + 'conversation_id = excluded.conversation_id;', + [serverId, conversationId], + ); + }); + } + + /// Rewrites one `setting` list whose entries this migration renamed. + /// + /// `serverOrder` holds ids and `snippetOrder` holds names, and both could + /// change above. [resolve] answers what an entry became, or null if it named + /// a record that is not here — those are dropped. + void _rewriteOrder(String key, String? Function(String) resolve) { + final rows = _db.select( + "SELECT value FROM kv WHERE store = 'setting' AND key = ?;", + [key], + ); + if (rows.isEmpty) return; + final List entries; + try { + final decoded = json.decode(rows.single['value'] as String); + if (decoded is! List) return; + entries = decoded.whereType().toList(); + } catch (e) { + Loggers.app.warning('m004: $key unreadable', e); + return; + } + + final rewritten = [ + for (final entry in entries) ?resolve(entry), + ]; + if (rewritten.length == entries.length && + rewritten.indexed.every((e) => e.$2 == entries[e.$1])) { + return; + } + // Not an edit by the user, so the timestamp stays where it was: stamping + // it would tell the next sync this device holds the newer copy. + _db.execute( + "UPDATE kv SET value = ? WHERE store = 'setting' AND key = ?;", + [json.encode(rewritten), key], + ); + } +} diff --git a/lib/data/store/port_forward.dart b/lib/data/store/port_forward.dart index e5f050b968..08bed1f97b 100644 --- a/lib/data/store/port_forward.dart +++ b/lib/data/store/port_forward.dart @@ -1,39 +1,90 @@ import 'package:fl_lib/fl_lib.dart'; +import 'package:meta/meta.dart'; import 'package:server_box/data/model/server/port_forward.dart'; -import 'package:server_box/data/store/server.dart'; +import 'package:server_box/data/store/entity_store.dart'; +import 'package:sqlite3/sqlite3.dart'; -class PortForwardStore extends SqliteStore { - PortForwardStore._() : super('port_forward'); +/// Port forwards, as rows in `port_forward`. +/// +/// `server_id` is a foreign key that cascades, so deleting a server takes its +/// forwards with it. Nothing did that before, and [fetch] answered "this +/// server's forwards" by decoding every record in the store. +class PortForwardStore extends EntityStore { + PortForwardStore._(); - /// The same seam [ServerStore.forTest] has: a distinct store name, so a test - /// tree writing here cannot touch what the app stores. - PortForwardStore.forTest() : super('port_forward_test'); + /// See [PrivateKeyStore.forTest]. + @visibleForTesting + PortForwardStore.forTest(); static final instance = PortForwardStore._(); - void put(PortForwardConfig config) { - set(config.id, config); - } + @override + String get table => 'port_forward'; + + @override + String idOf(PortForwardConfig item) => item.id; + + @override + String? nameOf(PortForwardConfig item) => item.name; + + @override + List readAll() => + db.select('SELECT * FROM port_forward;').map(_fromRow).toList(); - List fetch(String serverId) { - final configs = []; - // One query, not one per key: `getAllMap` reads the store in a single - // statement. - for (final raw in getAllMap().values) { - if (raw is! Map) continue; - final PortForwardConfig config; - try { - config = PortForwardConfig.fromJson(Map.from(raw)); - } catch (e) { - dprint('Parsing PortForwardConfig from JSON', e); - continue; - } - if (config.serverId == serverId) configs.add(config); + static PortForwardConfig _fromRow(Row row) => PortForwardConfig( + id: row['id'] as String, + serverId: row['server_id'] as String, + name: row['name'] as String, + type: PortForwardType.values.firstWhere( + (e) => e.name == row['type'], + orElse: () => PortForwardType.local, + ), + localHost: row['local_host'] as String?, + localPort: row['local_port'] as int? ?? 0, + remoteHost: row['remote_host'] as String?, + remotePort: row['remote_port'] as int?, + ); + + @override + void write(PortForwardConfig item) => upsert( + const [ + 'id', + 'server_id', + 'name', + 'type', + 'local_host', + 'local_port', + 'remote_host', + 'remote_port', + ], + [ + item.id, + item.serverId, + item.name, + item.type.name, + item.localHost, + item.localPort, + item.remoteHost, + item.remotePort, + ], + ); + + @override + Map toJson(PortForwardConfig item) => item.toJson(); + + @override + PortForwardConfig? fromJson(Map json) { + try { + return PortForwardConfig.fromJson(json); + } catch (e) { + dprint('Parsing PortForwardConfig from JSON', e); + return null; } - return configs; } - void delete(PortForwardConfig config) { - remove(config.id); - } + /// One server's forwards, as an indexed query. + List fetchForServer(String serverId) => db + .select('SELECT * FROM port_forward WHERE server_id = ?;', [serverId]) + .map(_fromRow) + .toList(); } diff --git a/lib/data/store/private_key.dart b/lib/data/store/private_key.dart index 2e6bef399a..07cf8ece16 100644 --- a/lib/data/store/private_key.dart +++ b/lib/data/store/private_key.dart @@ -1,32 +1,82 @@ +import 'package:fl_lib/fl_lib.dart'; import 'package:meta/meta.dart'; - import 'package:server_box/data/model/server/private_key_info.dart'; -import 'package:server_box/data/store/cached_store.dart'; -import 'package:server_box/data/store/server.dart'; +import 'package:server_box/data/store/entity_store.dart'; +import 'package:sqlite3/sqlite3.dart'; -class PrivateKeyStore extends CachedSqliteStore { - PrivateKeyStore._() : super('key'); +/// Private keys, as rows in `private_key`. +/// +/// `id` is generated and `name` is what the user typed. They were the same +/// value until the tables landed, which is why renaming a key detached every +/// server pointing at it — `Spi.ssh.keyId` held the name. +class PrivateKeyStore extends EntityStore { + PrivateKeyStore._(); - /// See [ServerStore.forTest]. + /// A second instance over the same table. + /// + /// The table name is fixed by the schema now, so isolation between tests + /// comes from `SqliteDb.openInMemory()` being fresh per test. What this + /// still buys is a cache that is not the singleton's. @visibleForTesting - PrivateKeyStore.forTest() : super('key_test'); + PrivateKeyStore.forTest(); static final instance = PrivateKeyStore._(); @override - String getKey(PrivateKeyInfo item) => item.id; + String get table => 'private_key'; + + @override + String idOf(PrivateKeyInfo item) => item.id; + + @override + List readAll() => db + .select('SELECT id, name, key FROM private_key ORDER BY name;') + .map(_fromRow) + .toList(); + + static PrivateKeyInfo _fromRow(Row row) => PrivateKeyInfo( + id: row['id'] as String, + name: row['name'] as String, + key: row['key'] as String, + ); + + @override + void write(PrivateKeyInfo item) => + upsert(const ['id', 'name', 'key'], [item.id, item.name, item.key]); + + @override + String? nameOf(PrivateKeyInfo item) => item.name; + + @override + Map toJson(PrivateKeyInfo item) => item.toJson(); @override - PrivateKeyInfo? fromJson(Map json) => - PrivateKeyInfo.fromJson(json); + PrivateKeyInfo? fromJson(Map json) { + try { + return PrivateKeyInfo.fromJson(json); + } catch (e) { + dprint('Parsing PrivateKeyInfo from JSON', e); + return null; + } + } + + /// A key from a backup written before ids existed carries its name as its + /// id; keeping the one already here is what makes restoring twice a no-op. + @override + PrivateKeyInfo reconcile(PrivateKeyInfo incoming) { + if (fetchOneRaw(incoming.id) != null) return incoming; + final existing = fetchByName(incoming.name); + return existing == null ? incoming : incoming.copyWith(id: existing.id); + } + + PrivateKeyInfo? fetchOne(String? id) => id == null ? null : fetchOneRaw(id); - PrivateKeyInfo? fetchOne(String? id) { - if (id == null) return null; - if (cachedItems != null) { - for (final pki in cachedItems!) { - if (pki.id == id) return pki; - } + /// The key called [name], for the places that only have one — an imported + /// `~/.ssh/config`, and the backup format that keyed keys by name. + PrivateKeyInfo? fetchByName(String name) { + for (final key in fetch()) { + if (key.name == name) return key; } - return fetchOneRaw(id); + return null; } } diff --git a/lib/data/store/schema.dart b/lib/data/store/schema.dart index 6475814faf..3d41d50218 100644 --- a/lib/data/store/schema.dart +++ b/lib/data/store/schema.dart @@ -59,7 +59,9 @@ abstract final class SchemaVersion { /// only place a pre-v3 record can be decoded at all; it records [current] /// when it finishes. Every install therefore reaches SQLite already at v4, /// and [migrate] has nothing to do until a v5 exists. - static const current = 4; + /// v5: entities out of `kv` and into tables with columns, foreign keys + /// and per-row sync metadata + static const current = 5; /// Persisted locally, never included in a backup: it describes *this /// device's* storage, and restoring another device's number would make the @@ -98,4 +100,18 @@ abstract final class SchemaVersion { /// Marks a fresh install as already current, so its empty stores aren't put /// through migrations written for data that doesn't exist. static void initFresh() => _store(current); + + /// The layout `HiveImport` produces: entities as JSON rows in `kv`. + /// + /// The import is not a fresh install even though it starts from an empty + /// database — it writes the shape that was current when Hive was dropped, + /// which later steps still have to convert. Recording [current] instead + /// would tell the migrator there is nothing to do and strand every record + /// in the shape it landed in. + /// + /// TODO: delete with `HiveImport`, and bump this in step with it until then. + static const hiveImportProduces = 4; + + /// Marks the layout [HiveImport] wrote, so the steps after it still run. + static void initAtHiveImport() => _store(hiveImportProduces); } diff --git a/lib/data/store/server.dart b/lib/data/store/server.dart index 46215b2ded..4b3c2c84ba 100644 --- a/lib/data/store/server.dart +++ b/lib/data/store/server.dart @@ -1,166 +1,336 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:meta/meta.dart'; -import 'package:server_box/data/model/container/type.dart'; +import 'package:server_box/data/model/server/custom.dart'; +import 'package:server_box/data/model/server/monitor_http_credential.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; -import 'package:server_box/data/store/cached_store.dart'; -import 'package:server_box/data/store/container.dart'; -import 'package:server_box/data/store/private_key.dart'; -import 'package:server_box/data/store/setting.dart'; -import 'package:server_box/data/store/snippet.dart'; +import 'package:server_box/data/model/server/ssh_credential.dart'; +import 'package:server_box/data/model/server/system.dart'; +import 'package:server_box/data/model/server/wol_cfg.dart'; +import 'package:server_box/data/store/entity_store.dart'; +import 'package:sqlite3/sqlite3.dart'; -class ServerStore extends CachedSqliteStore { - ServerStore._() : super('server'); +/// Servers, as rows in `server` plus the child tables hanging off it. +/// +/// A [Spi] is assembled from six tables, but never six queries per server: +/// [readAll] reads each child table once and groups in Dart, so the cost is +/// the number of tables rather than the number of servers. +class ServerStore extends EntityStore { + ServerStore._(); - /// The same seam [SettingStore.forTest] has: a distinct store name, so a test - /// on `SqliteDb.openInMemory()` cannot collide with another test's rows. + /// See [PrivateKeyStore.forTest]. @visibleForTesting - ServerStore.forTest() : super('server_test'); + ServerStore.forTest(); static final instance = ServerStore._(); @override - String getKey(Spi item) => item.id; + String get table => 'server'; @override - Spi? fromJson(Map json) => Spi.fromJson(json); + String idOf(Spi item) => item.id; - /// Moves an `IdentityFile` path out of `keyId` and into `keyPath`. - /// - /// `~/.ssh/config` import used to write the path into the field every reader - /// looks up as a `Stores.key` id, so those servers could only ever fail to - /// connect and showed no key in the edit page. See `SshCredential.keyPath`. - /// - /// Decided against the store rather than by shape alone, because a key's id - /// is its user-typed name and somebody may have called one `~/keys/prod`. - /// A value that *is* an id therefore always wins. A value that names no key - /// and looks like a path is the bug — or a key the user has since deleted, - /// in which case the record is already broken and a path-shaped value is - /// better shown as a path than looked up forever. - /// - /// Known gap: `IdentityFile id_ed25519` with no separator at all is legal in - /// an ssh config, and such a record is indistinguishable from a key named - /// `id_ed25519` — so it stays where it is. Moving it would need a rule for - /// what a bare name is relative to, and OpenSSH's answer to that is not one - /// to guess at; it belongs at the parse site, on values this app has not - /// stored yet, rather than here on ones it has. - /// - /// TODO: remove once no stored record can predate the fix. - /// - /// [keys] is a seam for tests, the same shape [forBox] is: the decision needs - /// the key store, and a test's is not the singleton. - void migrateIdentityFilePaths({PrivateKeyStore? keys}) { - final keyStore = keys ?? PrivateKeyStore.instance; - for (final spi in fetch()) { - final ssh = spi.ssh; - final keyId = ssh?.keyId; - if (ssh == null || keyId == null) continue; - if (!_looksLikePath(keyId)) continue; - if (keyStore.fetchOne(keyId) != null) continue; - - update( - spi, - spi.copyWith(ssh: ssh.copyWith(keyId: null, keyPath: keyId)), - ); - Loggers.app.info('Moved IdentityFile path out of keyId for ${spi.name}'); + @override + String? nameOf(Spi item) => item.name; + + @override + List readAll() { + final rows = db.select('SELECT * FROM server;'); + if (rows.isEmpty) return const []; + + final tags = _group('SELECT server_id, tag FROM server_tag;', 'tag'); + final disabled = _group( + 'SELECT server_id, cmd_type FROM server_disabled_cmd;', + 'cmd_type', + ); + final jumps = _group( + 'SELECT server_id, jump_id FROM server_jump ORDER BY ord;', + 'jump_id', + ); + final envs = _pairs('SELECT server_id, key, value FROM server_env;', 'key', + 'value'); + final cmds = _pairs( + 'SELECT server_id, name, cmd FROM server_custom_cmd;', + 'name', + 'cmd', + ); + + return [ + for (final row in rows) + _toSpi( + row, + tags: tags[row['id']], + disabled: disabled[row['id']], + jumps: jumps[row['id']], + envs: envs[row['id']], + cmds: cmds[row['id']], + ), + ]; + } + + Map> _group(String sql, String column) { + final out = >{}; + for (final row in db.select(sql)) { + (out[row['server_id'] as String] ??= []).add(row[column] as String); + } + return out; + } + + Map> _pairs(String sql, String k, String v) { + final out = >{}; + for (final row in db.select(sql)) { + (out[row['server_id'] as String] ??= {})[row[k] as String] = + row[v] as String; } + return out; } - /// Whether [value] is a filesystem path rather than a key id. + static Spi _toSpi( + Row row, { + List? tags, + List? disabled, + List? jumps, + Map? envs, + Map? cmds, + }) { + final sshIp = row['ssh_ip'] as String?; + final monitorAddr = row['monitor_addr'] as String?; + return Spi( + id: row['id'] as String, + name: row['name'] as String, + autoConnect: (row['auto_connect'] as int) == 1, + customSystemType: SystemType.values.firstWhereOrNull( + (e) => e.name == row['system_type'], + ), + tags: tags, + disabledCmdTypes: disabled, + envs: envs, + ssh: sshIp == null + ? null + : SshCredential( + ip: sshIp, + port: row['ssh_port'] as int? ?? 22, + user: row['ssh_user'] as String? ?? 'root', + pwd: row['ssh_pwd'] as String?, + keyId: row['ssh_key_id'] as String?, + keyPath: row['ssh_key_path'] as String?, + alterUrl: row['ssh_alter_url'] as String?, + proxyCommand: row['ssh_proxy_command'] as String?, + jumpId: jumps?.firstOrNull, + jumpIds: jumps, + ), + monitorHttp: monitorAddr == null + ? null + : MonitorHttpCredential( + addr: monitorAddr, + user: row['monitor_user'] as String?, + pwd: row['monitor_pwd'] as String?, + ignoreCert: (row['monitor_ignore_cert'] as int? ?? 0) == 1, + ), + wolCfg: row['wol_mac'] == null + ? null + : WakeOnLanCfg( + mac: row['wol_mac'] as String, + ip: row['wol_ip'] as String? ?? '', + pwd: row['wol_pwd'] as String?, + ), + custom: _customOf(row, cmds), + ); + } + + /// Null when the record says nothing beyond the defaults. /// - /// `ShortId`'s alphabet is `0-9a-zA-Z-+`, so a generated id can match none of - /// these; a user-typed key name can, which is why the caller checks the store - /// as well. - static bool _looksLikePath(String value) => - value.startsWith('~') || value.contains('/') || value.contains(r'\'); - - void migrateIds() { - final ss = fetch(); - final idMap = {}; - - for (final s in ss) { - final newId = s.migrateId(); - if (newId == null) continue; - idMap[s.oldId] = newId; + /// The columns always have a value — `pve_ignore_cert` and `temp_is_celsius` + /// are `NOT NULL` with one — so building a [ServerCustom] unconditionally + /// would give every server a non-null `custom` it did not have before, and + /// put it in every backup. + static ServerCustom? _customOf(Row row, Map? cmds) { + const strings = [ + 'pve_addr', + 'pve_pwd', + 'prefer_temp_dev', + 'logo_url', + 'net_dev', + 'script_dir', + ]; + final pveIgnoreCert = (row['pve_ignore_cert'] as int) == 1; + final tempIsCelsius = (row['temp_is_celsius'] as int) == 1; + // Field by field rather than against `const ServerCustom()`: this is a + // plain class with no `==`, so comparing would always say "different". + if (cmds == null && + !pveIgnoreCert && + tempIsCelsius && + strings.every((c) => row[c] == null)) { + return null; + } + return ServerCustom( + pveAddr: row['pve_addr'] as String?, + pveIgnoreCert: pveIgnoreCert, + pvePwd: row['pve_pwd'] as String?, + cmds: cmds, + preferTempDev: row['prefer_temp_dev'] as String?, + tempIsCelsius: tempIsCelsius, + logoUrl: row['logo_url'] as String?, + netDev: row['net_dev'] as String?, + scriptDir: row['script_dir'] as String?, + ); + } + + @override + void write(Spi item) { + final ssh = item.ssh; + final monitor = item.monitorHttp; + final custom = item.custom; + const columns = [ + 'id', 'name', 'auto_connect', 'system_type', + 'ssh_ip', 'ssh_port', 'ssh_user', 'ssh_pwd', 'ssh_key_id', + 'ssh_key_path', 'ssh_alter_url', 'ssh_proxy_command', + 'monitor_addr', 'monitor_user', 'monitor_pwd', 'monitor_ignore_cert', + 'wol_mac', 'wol_ip', 'wol_pwd', + 'pve_addr', 'pve_ignore_cert', 'pve_pwd', 'prefer_temp_dev', + 'temp_is_celsius', 'logo_url', 'net_dev', 'script_dir', + ]; + upsert(columns, [ + item.id, + item.name, + item.autoConnect ? 1 : 0, + item.customSystemType?.name, + ssh?.ip, + ssh?.port, + ssh?.user, + ssh?.pwd, + ssh?.keyId, + ssh?.keyPath, + ssh?.alterUrl, + ssh?.proxyCommand, + monitor?.addr, + monitor?.user, + monitor?.pwd, + monitor == null ? null : (monitor.ignoreCert ? 1 : 0), + item.wolCfg?.mac, + item.wolCfg?.ip, + item.wolCfg?.pwd, + custom?.pveAddr, + (custom?.pveIgnoreCert ?? false) ? 1 : 0, + custom?.pvePwd, + custom?.preferTempDev, + (custom?.tempIsCelsius ?? true) ? 1 : 0, + custom?.logoUrl, + custom?.netDev, + custom?.scriptDir, + ]); + + // Replaced wholesale rather than diffed: the record arrives as one object, + // so what it does not carry is what was removed. + for (final t in const [ + 'server_tag', + 'server_env', + 'server_jump', + 'server_disabled_cmd', + 'server_custom_cmd', + ]) { + db.execute('DELETE FROM $t WHERE server_id = ?;', [item.id]); } - final srvOrder = SettingStore.instance.serverOrder.fetch(); - final snippets = SnippetStore.instance.fetch(); - final container = ContainerStore.instance; - - bool srvOrderChanged = false; - for (final e in idMap.entries) { - final oldId = e.key; - final newId = e.value; - - final srvIdx = srvOrder.indexOf(oldId); - if (srvIdx != -1) { - srvOrder[srvIdx] = newId; - srvOrderChanged = true; - } - - final spi = fetchOneRaw(newId); - if (spi != null) { - final newSpi = _replaceJumpIds(spi, idMap); - if (newSpi != null) { - update(spi, newSpi); - } - } - - for (final snippet in snippets) { - final autoRunsOn = snippet.autoRunOn; - final idx = autoRunsOn?.indexOf(oldId); - if (idx != null && idx != -1) { - final newAutoRunsOn = List.from(autoRunsOn ?? []); - newAutoRunsOn[idx] = newId; - final newSnippet = snippet.copyWith(autoRunOn: newAutoRunsOn); - SnippetStore.instance.update(snippet, newSnippet); - } - } - - final dockerHost = container.fetch(oldId, ContainerType.docker); - if (dockerHost != null) { - container.removeHost(oldId, ContainerType.docker); - container.put(newId, ContainerType.docker, dockerHost); - } + for (final tag in item.tags ?? const []) { + db.execute('INSERT OR IGNORE INTO server_tag VALUES (?, ?);', [ + item.id, + tag, + ]); } + (item.envs ?? const {}).forEach((k, v) { + db.execute('INSERT OR IGNORE INTO server_env VALUES (?, ?, ?);', [ + item.id, + k, + v, + ]); + }); + for (final cmd in item.disabledCmdTypes ?? const []) { + db.execute('INSERT OR IGNORE INTO server_disabled_cmd VALUES (?, ?);', [ + item.id, + cmd, + ]); + } + (custom?.cmds ?? const {}).forEach((k, v) { + db.execute('INSERT OR IGNORE INTO server_custom_cmd VALUES (?, ?, ?);', [ + item.id, + k, + v, + ]); + }); + + } - for (final spi in ss) { - if (get(spi.id) == null) continue; - final newSpi = _replaceJumpIds(spi, idMap); - if (newSpi != null) { - update(spi, newSpi); - } + /// A jump host is a server, so the row it names may not have been written + /// yet — during a restore the order is whatever the backup listed. This runs + /// once every server exists. + /// + /// One that is still missing is dropped rather than written as a dangling + /// reference; the foreign key would refuse it anyway, and refusing would + /// fail the whole restore over a server deleted long ago. + @override + void writeLinks(Spi item) { + var ord = 0; + for (final jump in item.ssh?.resolvedJumpIds ?? const []) { + final exists = db + .select('SELECT 1 FROM server WHERE id = ?;', [jump]) + .isNotEmpty; + if (!exists) continue; + db.execute('INSERT INTO server_jump VALUES (?, ?, ?);', [ + item.id, + ord++, + jump, + ]); } + } - if (srvOrderChanged) { - SettingStore.instance.serverOrder.put(srvOrder); + @override + Map toJson(Spi item) => item.toJson(); + + @override + Spi? fromJson(Map json) { + try { + return Spi.fromJson(json); + } catch (e) { + dprint('Parsing Spi from JSON', e); + return null; } } -} -Spi? _replaceJumpIds(Spi spi, Map idMap) { - final ssh = spi.ssh; - if (ssh == null) return null; - - var changed = false; - final newJumpIds = ssh.resolvedJumpIds.map((id) { - final newId = idMap[id]; - if (newId == null) return id; - changed = true; - return newId; - }).toList(); - - final oldJumpId = ssh.jumpId; - final newJumpId = oldJumpId != null && idMap.containsKey(oldJumpId) - ? idMap[oldJumpId] - : oldJumpId; - changed = changed || newJumpId != oldJumpId; - - if (!changed) return null; - return spi.copyWith( - ssh: ssh.copyWith( - jumpId: newJumpIds.isEmpty ? newJumpId : newJumpIds.first, - jumpIds: newJumpIds.isEmpty ? null : newJumpIds, - ), - ); + /// Servers carrying [tag], as a query rather than a decode of every record. + List idsWithTag(String tag) => db + .select('SELECT server_id FROM server_tag WHERE tag = ?;', [tag]) + .map((r) => r['server_id'] as String) + .toList(); + + /// Every tag in use, for the filter bar. + List allTags() => db + .select('SELECT DISTINCT tag FROM server_tag ORDER BY tag;') + .map((r) => r['tag'] as String) + .toList(); + + /// The trusted host keys for [serverId], which go when the server does. + Map knownHosts(String serverId) => { + for (final row in db.select( + 'SELECT key_type, fingerprint FROM known_host WHERE server_id = ?;', + [serverId], + )) + row['key_type'] as String: row['fingerprint'] as String, + }; + + void trustHost(String serverId, String keyType, String fingerprint) { + db.execute('INSERT OR REPLACE INTO known_host VALUES (?, ?, ?);', [ + serverId, + keyType, + fingerprint, + ]); + touch(serverId); + } + + void forgetHost(String serverId, String keyType) { + db.execute('DELETE FROM known_host WHERE server_id = ? AND key_type = ?;', [ + serverId, + keyType, + ]); + touch(serverId); + } } diff --git a/lib/data/store/snippet.dart b/lib/data/store/snippet.dart index c4b346f436..337b4c71a0 100644 --- a/lib/data/store/snippet.dart +++ b/lib/data/store/snippet.dart @@ -1,20 +1,164 @@ +import 'package:fl_lib/fl_lib.dart'; import 'package:meta/meta.dart'; import 'package:server_box/data/model/server/snippet.dart'; -import 'package:server_box/data/store/cached_store.dart'; +import 'package:server_box/data/store/entity_store.dart'; -class SnippetStore extends CachedSqliteStore { - SnippetStore._() : super('snippet'); +/// Snippets, as rows in `snippet` plus its two child tables. +/// +/// `tags` and `autoRunOn` were JSON arrays inside the record, so "which +/// snippets run on this server" meant decoding every snippet, and deleting a +/// server left it named in the ones that did. Both are rows that cascade now. +class SnippetStore extends EntityStore { + SnippetStore._(); - /// The same seam [ServerStore.forTest] has: a distinct store name, so a test - /// on `SqliteDb.openInMemory()` cannot collide with another test's rows. + /// See [PrivateKeyStore.forTest]. @visibleForTesting - SnippetStore.forTest() : super('snippet_test'); + SnippetStore.forTest(); static final instance = SnippetStore._(); @override - String getKey(Snippet item) => item.name; + String get table => 'snippet'; @override - Snippet? fromJson(Map json) => Snippet.fromJson(json); + String idOf(Snippet item) => item.id; + + @override + String? nameOf(Snippet item) => item.name; + + @override + List readAll() { + final rows = db.select('SELECT id, name, script, note FROM snippet;'); + if (rows.isEmpty) return const []; + + final tags = _group('SELECT snippet_id, tag FROM snippet_tag;', 'tag'); + final autoRun = _group( + 'SELECT snippet_id, server_id FROM snippet_auto_run_on;', + 'server_id', + ); + + return [ + for (final row in rows) + Snippet( + id: row['id'] as String, + name: row['name'] as String, + script: row['script'] as String, + note: row['note'] as String?, + tags: tags[row['id']], + autoRunOn: autoRun[row['id']], + ), + ]; + } + + Map> _group(String sql, String column) { + final out = >{}; + for (final row in db.select(sql)) { + (out[row['snippet_id'] as String] ??= []).add(row[column] as String); + } + return out; + } + + @override + void write(Snippet item) { + upsert( + const ['id', 'name', 'script', 'note'], + [item.id, item.name, item.script, item.note], + ); + + // Replaced wholesale rather than diffed: the record arrives as one object, + // so what it does not carry is what was removed. + db.execute('DELETE FROM snippet_tag WHERE snippet_id = ?;', [item.id]); + db.execute('DELETE FROM snippet_auto_run_on WHERE snippet_id = ?;', [ + item.id, + ]); + + for (final tag in item.tags ?? const []) { + db.execute('INSERT OR IGNORE INTO snippet_tag VALUES (?, ?);', [ + item.id, + tag, + ]); + } + for (final serverId in item.autoRunOn ?? const []) { + // A server that is not there is dropped rather than written as a + // dangling reference — the foreign key would refuse it anyway, and + // refusing would fail the whole save over a server deleted long ago. + final exists = db + .select('SELECT 1 FROM server WHERE id = ?;', [serverId]) + .isNotEmpty; + if (!exists) continue; + db.execute('INSERT OR IGNORE INTO snippet_auto_run_on VALUES (?, ?);', [ + item.id, + serverId, + ]); + } + } + + @override + Map toJson(Snippet item) => item.toJson(); + + @override + Snippet? fromJson(Map json) { + try { + return Snippet.fromJson(json); + } catch (e) { + dprint('Parsing Snippet from JSON', e); + return null; + } + } + + /// A snippet from a backup written before ids existed gets a fresh one from + /// `Snippet.fromJson` on every decode, so the name is what identifies it. + @override + Snippet reconcile(Snippet incoming) { + if (fetchOneRaw(incoming.id) != null) return incoming; + final existing = fetchByName(incoming.name); + return existing == null ? incoming : incoming.copyWith(id: existing.id); + } + + /// The snippet called [name]. The UI keeps names unique and `snippetOrder` + /// is a list of them, so this is how an order entry finds its record. + Snippet? fetchByName(String name) { + for (final snippet in fetch()) { + if (snippet.name == name) return snippet; + } + return null; + } + + /// Snippets to run when [serverId] connects, as a query rather than a decode + /// of every record. + List autoRunIdsFor(String serverId) => db + .select('SELECT snippet_id FROM snippet_auto_run_on WHERE server_id = ?;', [ + serverId, + ]) + .map((r) => r['snippet_id'] as String) + .toList(); + + /// Every tag in use, for the filter bar. + List allTags() => db + .select('SELECT DISTINCT tag FROM snippet_tag ORDER BY tag;') + .map((r) => r['tag'] as String) + .toList(); + + /// Renames a tag across every snippet carrying it, in one statement. + /// + /// The parents are stamped so the change is one the sync can see. + void renameTag(String from, String to) { + SqliteStore.transact(() { + final owners = db + .select('SELECT snippet_id FROM snippet_tag WHERE tag = ?;', [from]) + .map((r) => r['snippet_id'] as String) + .toList(); + if (owners.isEmpty) return; + // `OR REPLACE`, since a snippet may already carry the target tag and + // (snippet_id, tag) is the primary key. + db.execute('UPDATE OR REPLACE snippet_tag SET tag = ? WHERE tag = ?;', [ + to, + from, + ]); + for (final id in owners) { + synced.stamp(id); + } + }); + invalidate(); + } } diff --git a/lib/data/store/tables.dart b/lib/data/store/tables.dart new file mode 100644 index 0000000000..0fe27c5d1e --- /dev/null +++ b/lib/data/store/tables.dart @@ -0,0 +1,116 @@ +import 'package:drift/native.dart'; +import 'package:fl_lib/fl_lib.dart'; +import 'package:server_box/data/store/db.dart'; +import 'package:sqlite3/sqlite3.dart'; + +/// The names of the entity tables, for code that has to talk about them +/// rather than query them. +/// +/// The DDL itself lives in `db.dart`: Drift owns it, and `tables_schema_test +/// .dart` is the acceptance test that what Drift creates keeps every +/// guarantee — the SSH-xor-monitor CHECK, the cascades, the unique names. +/// Two sources for one schema is the failure this change exists to stop. +/// +/// What is *not* here: `setting` and `history`. Those are key-value by nature — +/// 103 unrelated preferences with no relations between them and nothing that +/// queries by field — so they stay rows in `kv`, and adding a preference stays +/// a one-line change rather than a schema migration. +/// +/// Conventions, both of which the old key-value layout could not hold: +/// +/// - **A primary key is an id, never something the user typed.** Snippets were +/// keyed by their name and private keys by a "name as id", so renaming +/// either silently broke every reference to it — `Spi.ssh.keyId` pointed at +/// a private key's name. Names are ordinary columns now, `UNIQUE` where the +/// UI requires it, and rename is an `UPDATE` of one column. +/// - **A list or map field is a child table**, not a JSON array in a column. +/// That is what makes "every server with this tag" a query rather than a +/// decode of every row, and what lets `ON DELETE CASCADE` clean up after a +/// deleted server instead of the six hand-written calls that used to. +abstract final class Tables { + /// Tables holding one logical record each, and so the unit of sync. + /// + /// A server and its tags, envs, jump hosts and container settings move + /// together: the children have no independent meaning and cascade with the + /// parent, so only the parent carries the sync columns and editing a child + /// bumps the parent's `updated_at`. Syncing children separately would mean a + /// tag could arrive before the server it belongs to. + /// + /// `conn_stat` and `agent_conversation` are absent on purpose. Connecting to + /// a server is not an edit, and a conversation carries terminal output and + /// reasoning — neither leaves the device. + static const syncRoots = [ + 'private_key', + 'server', + 'snippet', + 'port_forward', + ]; + + /// Every table the app owns, dependants after what they depend on. + /// + /// Used by the migration to write into them, and by tests to assert the + /// database has nothing else in it. + static const names = [ + 'private_key', + 'server', + 'server_tag', + 'server_env', + 'server_jump', + 'server_disabled_cmd', + 'server_custom_cmd', + 'known_host', + 'container_host', + 'container_runtime', + 'snippet', + 'snippet_tag', + 'snippet_auto_run_on', + 'port_forward', + 'conn_stat', + 'agent_conversation', + 'agent_active_conversation', + 'tombstone', + 'sync_state', + ]; +} + +/// Creates the entity schema on [db] by opening a Drift database over it. +/// +/// The DDL lives in Drift, but a migration and a test both need the tables to +/// exist on a raw handle before any Drift object has been built over it. This +/// is that seam: open, let `onCreate` run, and hand the connection back. +Future createTables(Database db) async { + if (!identical(_over, db)) { + // One live `AppDb` at a time. Two over the same connection is what Drift + // warns about, and a test suite that opens a database per test would + // otherwise leave one behind for each. + // + // Best-effort: the previous connection may already be closed — a test's + // `tearDown` closes it directly — and closing a wrapper around a dead + // handle is not worth failing the next open over. + try { + await _appDb?.close(); + } catch (e) { + Loggers.app.warning('Dropping the previous AppDb', e); + } + // `closeUnderlyingOnClose: false`: the handle belongs to `SqliteDb`, which + // opened it, applied the cipher pragmas and will close it. + _appDb = AppDb(NativeDatabase.opened(db, closeUnderlyingOnClose: false)); + _over = db; + } + // Nothing exists until the executor opens, and opening is what runs + // `onCreate`. Idempotent after that: Drift records its own version in + // `user_version` and `createAll` is `IF NOT EXISTS` regardless. + await _appDb!.customStatement('SELECT 1;'); +} + +/// Forgets the cached [AppDb] without touching any connection. +/// +/// For a caller that closes the handle itself, so the next [createTables] does +/// not reach through a wrapper around a database that is gone. +void resetTables() { + _appDb = null; + _over = null; +} + +AppDb? _appDb; +Database? _over; diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 5b819dc9df..f51a4a4e81 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -1251,6 +1251,12 @@ abstract class AppLocalizations { /// **'Jump servers not found for {serverName}: {jumpIds}'** String jumpServersNotFoundFmt(Object serverName, Object jumpIds); + /// No description provided for @nameAlreadyExistsFmt. + /// + /// In en, this message translates to: + /// **'\"{name}\" already exists'** + String nameAlreadyExistsFmt(Object name); + /// No description provided for @noJumpServerAvailable. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index b562a834ed..331ad67615 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -652,6 +652,11 @@ class AppLocalizationsDe extends AppLocalizations { return 'Jump-Server für $serverName nicht gefunden: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '„$name“ existiert bereits'; + } + @override String get noJumpServerAvailable => 'Kein Jump-Server verfügbar.'; diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index fdfde921c9..edb30d5e48 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -645,6 +645,11 @@ class AppLocalizationsEn extends AppLocalizations { return 'Jump servers not found for $serverName: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '\"$name\" already exists'; + } + @override String get noJumpServerAvailable => 'No jump server available.'; diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index d7786ff9a6..86625ec3f2 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -656,6 +656,11 @@ class AppLocalizationsEs extends AppLocalizations { return 'No se encontraron servidores de salto para $serverName: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '«$name» ya existe'; + } + @override String get noJumpServerAvailable => 'No hay ningún servidor de salto disponible.'; diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index c29bc25338..60c6a1b4f1 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -657,6 +657,11 @@ class AppLocalizationsFr extends AppLocalizations { return 'Serveurs de rebond introuvables pour $serverName : $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '« $name » existe déjà'; + } + @override String get noJumpServerAvailable => 'Aucun serveur de rebond disponible.'; diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index c35aa9c36c..3df6203c7d 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -651,6 +651,11 @@ class AppLocalizationsId extends AppLocalizations { return 'Jump server tidak ditemukan untuk $serverName: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '\"$name\" sudah ada'; + } + @override String get noJumpServerAvailable => 'Tidak ada jump server yang tersedia.'; diff --git a/lib/generated/l10n/l10n_it.dart b/lib/generated/l10n/l10n_it.dart index 5af12d4d99..69a121c733 100644 --- a/lib/generated/l10n/l10n_it.dart +++ b/lib/generated/l10n/l10n_it.dart @@ -656,6 +656,11 @@ class AppLocalizationsIt extends AppLocalizations { return 'Jump server non trovati per $serverName: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '«$name» esiste già'; + } + @override String get noJumpServerAvailable => 'Nessun jump server disponibile.'; diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index cc6f794eff..8160f4ed2a 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -618,6 +618,11 @@ class AppLocalizationsJa extends AppLocalizations { return '$serverName の踏み台サーバーが見つかりません: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '「$name」は既に存在します'; + } + @override String get noJumpServerAvailable => '利用できる踏み台サーバーがありません。'; diff --git a/lib/generated/l10n/l10n_ko.dart b/lib/generated/l10n/l10n_ko.dart index 3b38d2bc47..cc972e5d53 100644 --- a/lib/generated/l10n/l10n_ko.dart +++ b/lib/generated/l10n/l10n_ko.dart @@ -617,6 +617,11 @@ class AppLocalizationsKo extends AppLocalizations { return '$serverName에 대한 점프 서버를 찾을 수 없습니다: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '\'$name\'은(는) 이미 존재합니다'; + } + @override String get noJumpServerAvailable => '사용 가능한 점프 서버가 없습니다.'; diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index 3ff394ce59..ee4099b41d 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -651,6 +651,11 @@ class AppLocalizationsNl extends AppLocalizations { return 'Jumpservers niet gevonden voor $serverName: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '\'$name\' bestaat al'; + } + @override String get noJumpServerAvailable => 'Geen jumpserver beschikbaar.'; diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index 304631f778..ac0503fad2 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -651,6 +651,11 @@ class AppLocalizationsPt extends AppLocalizations { return 'Servidores de salto não encontrados para $serverName: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '\"$name\" já existe'; + } + @override String get noJumpServerAvailable => 'Nenhum servidor de salto disponível.'; diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index 82d0389c1b..0fb6c5ab39 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -652,6 +652,11 @@ class AppLocalizationsRu extends AppLocalizations { return 'Промежуточные серверы для $serverName не найдены: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '«$name» уже существует'; + } + @override String get noJumpServerAvailable => 'Нет доступного промежуточного сервера.'; diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index a708e01366..fe1ab5ce58 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -653,6 +653,11 @@ class AppLocalizationsTr extends AppLocalizations { return '$serverName için atlama sunucuları bulunamadı: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '\"$name\" zaten mevcut'; + } + @override String get noJumpServerAvailable => 'Kullanılabilir atlama sunucusu yok.'; diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index 2bfed650e8..621bcd3b46 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -652,6 +652,11 @@ class AppLocalizationsUk extends AppLocalizations { return 'Проміжні сервери для $serverName не знайдено: $jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '«$name» вже існує'; + } + @override String get noJumpServerAvailable => 'Немає доступного проміжного сервера.'; diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index d7e3efe4a2..6579f82aec 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -605,6 +605,11 @@ class AppLocalizationsZh extends AppLocalizations { return '未找到 $serverName 配置的跳板服务器:$jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '「$name」已存在'; + } + @override String get noJumpServerAvailable => '没有可用的跳板服务器。'; @@ -1893,6 +1898,11 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { return '未找到 $serverName 配置的跳板伺服器:$jumpIds'; } + @override + String nameAlreadyExistsFmt(Object name) { + return '「$name」已存在'; + } + @override String get noJumpServerAvailable => '沒有可用的跳板伺服器。'; diff --git a/lib/hive/hive_adapters.dart b/lib/hive/hive_adapters.dart index f488915118..0b8ff78555 100644 --- a/lib/hive/hive_adapters.dart +++ b/lib/hive/hive_adapters.dart @@ -4,17 +4,17 @@ import 'package:server_box/data/model/app/net_view.dart'; import 'package:server_box/data/model/server/custom.dart'; import 'package:server_box/data/model/server/monitor_http_credential.dart'; import 'package:server_box/data/model/server/port_forward.dart'; -import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; -import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/model/server/ssh_credential.dart'; import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/wol_cfg.dart'; import 'package:server_box/data/model/ssh/virtual_key.dart'; +/// `PrivateKeyInfo` and `Snippet` are deliberately absent: both gained a field +/// the released boxes do not carry, so a generated adapter can no longer read +/// one. They are frozen types in `legacy_adapters.dart` instead — see the note +/// there before adding a model back to this list. @GenerateAdapters([ - AdapterSpec(), - AdapterSpec(), AdapterSpec(), AdapterSpec(), AdapterSpec(), diff --git a/lib/hive/hive_adapters.g.dart b/lib/hive/hive_adapters.g.dart index 1eea1e5c82..ebdb7e0d41 100644 --- a/lib/hive/hive_adapters.g.dart +++ b/lib/hive/hive_adapters.g.dart @@ -6,86 +6,6 @@ part of 'hive_adapters.dart'; // AdaptersGenerator // ************************************************************************** -class PrivateKeyInfoAdapter extends TypeAdapter { - @override - final typeId = 1; - - @override - PrivateKeyInfo read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return PrivateKeyInfo(id: fields[0] as String, key: fields[1] as String); - } - - @override - void write(BinaryWriter writer, PrivateKeyInfo obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.key); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is PrivateKeyInfoAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SnippetAdapter extends TypeAdapter { - @override - final typeId = 2; - - @override - Snippet read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Snippet( - name: fields[0] as String, - script: fields[1] as String, - tags: (fields[2] as List?)?.cast(), - note: fields[3] as String?, - autoRunOn: (fields[4] as List?)?.cast(), - ); - } - - @override - void write(BinaryWriter writer, Snippet obj) { - writer - ..writeByte(5) - ..writeByte(0) - ..write(obj.name) - ..writeByte(1) - ..write(obj.script) - ..writeByte(2) - ..write(obj.tags) - ..writeByte(3) - ..write(obj.note) - ..writeByte(4) - ..write(obj.autoRunOn); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SnippetAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - class VirtKeyAdapter extends TypeAdapter { @override final typeId = 4; diff --git a/lib/hive/hive_adapters.g.yaml b/lib/hive/hive_adapters.g.yaml index 990777ebe4..7cf390ba67 100644 --- a/lib/hive/hive_adapters.g.yaml +++ b/lib/hive/hive_adapters.g.yaml @@ -3,28 +3,6 @@ # Check in to version control nextTypeId: 17 types: - PrivateKeyInfo: - typeId: 1 - nextIndex: 2 - fields: - id: - index: 0 - key: - index: 1 - Snippet: - typeId: 2 - nextIndex: 5 - fields: - name: - index: 0 - script: - index: 1 - tags: - index: 2 - note: - index: 3 - autoRunOn: - index: 4 VirtKey: typeId: 4 nextIndex: 47 diff --git a/lib/hive/hive_registrar.g.dart b/lib/hive/hive_registrar.g.dart index 96db820fe3..3ec6755d16 100644 --- a/lib/hive/hive_registrar.g.dart +++ b/lib/hive/hive_registrar.g.dart @@ -16,11 +16,9 @@ extension HiveRegistrar on HiveInterface { registerAdapter(NetViewTypeAdapter()); registerAdapter(PortForwardConfigAdapter()); registerAdapter(PortForwardTypeAdapter()); - registerAdapter(PrivateKeyInfoAdapter()); registerAdapter(ServerConnectionStatsAdapter()); registerAdapter(ServerCustomAdapter()); registerAdapter(ServerFuncBtnAdapter()); - registerAdapter(SnippetAdapter()); registerAdapter(SpiAdapter()); registerAdapter(SshCredentialAdapter()); registerAdapter(SystemTypeAdapter()); @@ -38,11 +36,9 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface { registerAdapter(NetViewTypeAdapter()); registerAdapter(PortForwardConfigAdapter()); registerAdapter(PortForwardTypeAdapter()); - registerAdapter(PrivateKeyInfoAdapter()); registerAdapter(ServerConnectionStatsAdapter()); registerAdapter(ServerCustomAdapter()); registerAdapter(ServerFuncBtnAdapter()); - registerAdapter(SnippetAdapter()); registerAdapter(SpiAdapter()); registerAdapter(SshCredentialAdapter()); registerAdapter(SystemTypeAdapter()); diff --git a/lib/hive/legacy_adapters.dart b/lib/hive/legacy_adapters.dart new file mode 100644 index 0000000000..705410a2cf --- /dev/null +++ b/lib/hive/legacy_adapters.dart @@ -0,0 +1,116 @@ +import 'package:hive_ce/hive.dart'; +import 'package:server_box/hive/spi_legacy_adapter.dart'; + +/// Hive records as the released builds wrote them, frozen. +/// +/// Nothing writes Hive; these exist only so `HiveImport` can read what an +/// upgrading install has. That makes generating an adapter from a live model a +/// trap: the generator emits `fields[n] as String` for every non-nullable +/// field, so **adding one to a model makes every box written before it +/// unreadable** — the field is absent, the cast gets null, and the box fails to +/// open. `test/hive_release_migration_test.dart` is what catches it, and it +/// caught exactly this when `Snippet` gained an id and `PrivateKeyInfo` gained +/// a name. +/// +/// So a model that Hive still has an adapter for gets a frozen type here +/// instead, the same way `LegacySpiV2` was already handled. Each one: +/// +/// - carries the fields the release wrote, at the field numbers it wrote them +/// - is a type of its own, because Hive resolves *writes* by runtime type and +/// two adapters claiming one type make every write depend on registration +/// order +/// - converts to JSON in the shape that release's `toJson` produced, which is +/// what `KvToTablesMigration` reads. Not to today's model: a migration that +/// goes through the current code changes meaning every time that code does. +/// +/// TODO: delete this file, `lib/hive/` and the `hive_ce*` dependencies with +/// `HiveImport`, once no supported install can still be on Hive. +void registerHiveLegacyAdapters() { + Hive.registerAdapter(SpiLegacyAdapter()); + Hive.registerAdapter(LegacyPrivateKeyAdapter()); + Hive.registerAdapter(LegacySnippetAdapter()); +} + +/// typeId 1, as written up to and including v1.0.1491: an id that was also the +/// name, and the key itself. +class LegacyPrivateKeyV1 { + const LegacyPrivateKeyV1({required this.id, required this.key}); + + final String id; + final String key; + + /// `private_key`, not `key`: the released model carried + /// `@JsonKey(name: 'private_key')`. + Map toJson() => {'id': id, 'private_key': key}; +} + +class LegacyPrivateKeyAdapter extends TypeAdapter { + @override + final typeId = 1; + + @override + LegacyPrivateKeyV1 read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return LegacyPrivateKeyV1( + id: fields[0] as String, + key: fields[1] as String, + ); + } + + @override + void write(BinaryWriter writer, LegacyPrivateKeyV1 obj) => + throw UnsupportedError('Hive is read-only'); +} + +/// typeId 2, as written up to and including v1.0.1491: no id, because a snippet +/// was keyed by its name. +class LegacySnippetV1 { + const LegacySnippetV1({ + required this.name, + required this.script, + required this.tags, + required this.note, + required this.autoRunOn, + }); + + final String name; + final String script; + final List? tags; + final String? note; + final List? autoRunOn; + + Map toJson() => { + 'name': name, + 'script': script, + 'tags': tags, + 'note': note, + 'autoRunOn': autoRunOn, + }; +} + +class LegacySnippetAdapter extends TypeAdapter { + @override + final typeId = 2; + + @override + LegacySnippetV1 read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return LegacySnippetV1( + name: fields[0] as String, + script: fields[1] as String, + tags: (fields[2] as List?)?.cast(), + note: fields[3] as String?, + autoRunOn: (fields[4] as List?)?.cast(), + ); + } + + @override + void write(BinaryWriter writer, LegacySnippetV1 obj) => + throw UnsupportedError('Hive is read-only'); +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index d99c0240c4..c06b4a1875 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -159,6 +159,7 @@ }, "rootfsSubtitle": "Eine Linux-Userland-Umgebung auf diesem Gerät", "rootfsInstallTip": "Alpine Linux {version} (etwa 3 MB) herunterladen und auf diesem Gerät entpacken. Es gibt dieser App eine Shell mit Paketmanager und kann jederzeit gelöscht werden.", + "nameAlreadyExistsFmt": "„{name}“ existiert bereits", "sameIdServerExist": "Ein Server mit derselben ID existiert bereits", "second": "s", "serverFilesUnavailableTip": "Erreichbar entweder über SSH dieses Servers oder über einen Monitor-Agenten mit eingeschalteter Datei-API.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e336b1b51b..7c9ed711b6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -194,6 +194,12 @@ "jumpIds": {} } }, + "nameAlreadyExistsFmt": "\"{name}\" already exists", + "@nameAlreadyExistsFmt": { + "placeholders": { + "name": {} + } + }, "noJumpServerAvailable": "No jump server available.", "jumpServerAndProxyCommandCannotBeUsedTogether": "Jump server and ProxyCommand cannot be used together.", "keepForeground": "Keep app foreground!", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index bbe9542eee..724847312e 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -159,6 +159,7 @@ }, "rootfsSubtitle": "Un espacio de usuario Linux en este dispositivo", "rootfsInstallTip": "Descarga Alpine Linux {version} (unos 3 MB) y lo descomprime en este dispositivo. Le da a esta app un shell con gestor de paquetes, y se puede eliminar en cualquier momento.", + "nameAlreadyExistsFmt": "«{name}» ya existe", "sameIdServerExist": "Ya existe un servidor con el mismo ID", "second": "Segundo", "serverFilesUnavailableTip": "Accesible mediante el SSH de este servidor o mediante un agente monitor con su API de archivos activada.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 4b79f172b0..4f349efe2d 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -159,6 +159,7 @@ }, "rootfsSubtitle": "Un espace utilisateur Linux sur cet appareil", "rootfsInstallTip": "Télécharge Alpine Linux {version} (environ 3 Mo) et le décompresse sur cet appareil. Il donne à cette application un shell avec gestionnaire de paquets, et peut être supprimé à tout moment.", + "nameAlreadyExistsFmt": "« {name} » existe déjà", "sameIdServerExist": "Un serveur avec le même ID existe déjà", "second": "s", "serverFilesUnavailableTip": "Accessible via le SSH de ce serveur, ou via un agent monitor dont l'API de fichiers est activée.", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index b6c3979dc6..8cdfd5ca47 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -159,6 +159,7 @@ }, "rootfsSubtitle": "Lingkungan pengguna Linux di perangkat ini", "rootfsInstallTip": "Unduh Alpine Linux {version} (sekitar 3 MB) dan ekstrak di perangkat ini. Ini memberi aplikasi ini shell dengan manajer paket, dan dapat dihapus kapan saja.", + "nameAlreadyExistsFmt": "\"{name}\" sudah ada", "sameIdServerExist": "Server dengan ID yang sama sudah ada", "second": "S", "serverFilesUnavailableTip": "Dapat dijangkau melalui SSH server ini, atau melalui agen monitor dengan API berkasnya diaktifkan.", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index a43608632b..93164f6e3c 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -160,6 +160,7 @@ }, "rootfsSubtitle": "Uno spazio utente Linux su questo dispositivo", "rootfsInstallTip": "Scarica Alpine Linux {version} (circa 3 MB) e lo decomprime su questo dispositivo. Fornisce a questa app una shell con gestore di pacchetti e può essere eliminato in qualsiasi momento.", + "nameAlreadyExistsFmt": "«{name}» esiste già", "sameIdServerExist": "Esiste già un server con lo stesso ID", "second": "s", "serverFilesUnavailableTip": "Raggiungibile tramite l'SSH di questo server o tramite un agente monitor con la sua API dei file attiva.", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 64fe369692..e7d6456e5e 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -159,6 +159,7 @@ }, "rootfsSubtitle": "この端末上の Linux ユーザーランド", "rootfsInstallTip": "Alpine Linux {version}(約 3 MB)をダウンロードし、この端末に展開します。パッケージマネージャー付きのシェルをこのアプリに提供し、いつでも削除できます。", + "nameAlreadyExistsFmt": "「{name}」は既に存在します", "sameIdServerExist": "同じIDのサーバーが既に存在します", "second": "秒", "serverFilesUnavailableTip": "このサーバーの SSH に接続できるか、monitor エージェントのファイル API が有効になっている必要があります。", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 3a5889eaed..c6c5db295c 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -202,6 +202,7 @@ }, "rootfsSubtitle": "이 기기의 Linux 사용자 공간", "rootfsInstallTip": "Alpine Linux {version}(약 3 MB)을 내려받아 이 기기에 풉니다. 패키지 관리자가 있는 셸을 이 앱에 제공하며, 언제든지 삭제할 수 있습니다.", + "nameAlreadyExistsFmt": "'{name}'은(는) 이미 존재합니다", "sameIdServerExist": "동일한 ID의 서버가 이미 존재합니다", "second": "초", "serverFilesUnavailableTip": "이 서버의 SSH에 연결할 수 있거나, monitor 에이전트의 파일 API가 켜져 있어야 합니다.", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index ae39229374..964146863f 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -159,6 +159,7 @@ }, "rootfsSubtitle": "Een Linux-userland op dit apparaat", "rootfsInstallTip": "Download Alpine Linux {version} (ongeveer 3 MB) en pak het uit op dit apparaat. Het geeft deze app een shell met pakketbeheerder en kan op elk moment worden verwijderd.", + "nameAlreadyExistsFmt": "'{name}' bestaat al", "sameIdServerExist": "Er bestaat al een server met dezelfde ID", "second": "s", "serverFilesUnavailableTip": "Bereikbaar via de SSH van deze server, of via een monitor-agent met zijn bestands-API ingeschakeld.", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8feff8a3ba..c136f411a4 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -159,6 +159,7 @@ }, "rootfsSubtitle": "Um espaço de usuário Linux neste dispositivo", "rootfsInstallTip": "Baixa o Alpine Linux {version} (cerca de 3 MB) e o descompacta neste dispositivo. Dá a este app um shell com gerenciador de pacotes, e pode ser excluído a qualquer momento.", + "nameAlreadyExistsFmt": "\"{name}\" já existe", "sameIdServerExist": "Já existe um servidor com o mesmo ID", "second": "Segundo", "serverFilesUnavailableTip": "Acessível através do SSH deste servidor, ou através de um agente monitor com a sua API de ficheiros ativada.", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 84bb5aae38..cd81b152f8 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -159,6 +159,7 @@ }, "rootfsSubtitle": "Пользовательское окружение Linux на этом устройстве", "rootfsInstallTip": "Загрузить Alpine Linux {version} (около 3 МБ) и распаковать на этом устройстве. Это даёт приложению оболочку с пакетным менеджером; её можно удалить в любой момент.", + "nameAlreadyExistsFmt": "«{name}» уже существует", "sameIdServerExist": "Сервер с таким ID уже существует", "second": "с", "serverFilesUnavailableTip": "Доступно через SSH этого сервера или через агент monitor с включённым файловым API.", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 4b8d5dfb1c..f4b17c3a66 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -159,6 +159,7 @@ }, "rootfsSubtitle": "Bu cihazdaki bir Linux kullanıcı alanı", "rootfsInstallTip": "Alpine Linux {version} (yaklaşık 3 MB) indirilir ve bu cihazda açılır. Bu uygulamaya paket yöneticisi olan bir kabuk sağlar ve istediğiniz zaman silinebilir.", + "nameAlreadyExistsFmt": "\"{name}\" zaten mevcut", "sameIdServerExist": "Aynı kimliğe sahip bir sunucu zaten mevcut", "second": "s", "serverFilesUnavailableTip": "Bu sunucunun SSH'ı üzerinden ya da dosya API'si açık bir monitor aracısı üzerinden erişilebilir.", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 4b776d1400..1744c9e84a 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -160,6 +160,7 @@ }, "rootfsSubtitle": "Користувацьке середовище Linux на цьому пристрої", "rootfsInstallTip": "Завантажити Alpine Linux {version} (близько 3 МБ) і розпакувати на цьому пристрої. Це дає застосунку оболонку з менеджером пакунків; її можна будь-коли видалити.", + "nameAlreadyExistsFmt": "«{name}» вже існує", "sameIdServerExist": "Сервер з таким ID вже існує", "second": "сек.", "serverFilesUnavailableTip": "Доступно через SSH цього сервера або через агент monitor з увімкненим файловим API.", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 93bbf75e45..51586476eb 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -264,6 +264,7 @@ "version": {} } }, + "nameAlreadyExistsFmt": "「{name}」已存在", "sameIdServerExist": "已存在相同 id 的服务器", "second": "秒", "serverFilesUnavailableTip": "需要能连上这台服务器的 SSH,或者它的 monitor agent 开启了文件 API。", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index ad63600e7c..9ee8ba54c6 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -264,6 +264,7 @@ "version": {} } }, + "nameAlreadyExistsFmt": "「{name}」已存在", "sameIdServerExist": "已存在相同 ID 的伺服器", "second": "秒", "serverFilesUnavailableTip": "需要能連上這台伺服器的 SSH,或者它的 monitor agent 開啟了檔案 API。", diff --git a/lib/main.dart b/lib/main.dart index 5919017ac9..cf10cf94a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,10 +20,10 @@ import 'package:server_box/data/res/build_data.dart'; import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/ssh/session_manager.dart'; +import 'package:server_box/data/store/migrations/m004_kv_to_tables.dart'; import 'package:server_box/data/store/schema.dart'; -import 'package:server_box/data/store/server.dart'; import 'package:server_box/hive/hive_registrar.g.dart'; -import 'package:server_box/hive/spi_legacy_adapter.dart'; +import 'package:server_box/hive/legacy_adapters.dart'; import 'package:server_box/src/rust/frb_generated.dart'; Future main() async { @@ -138,7 +138,7 @@ Future _initData() async { Hive.registerAdapters(); // Reads pre-v3 server records, which the generated SpiAdapter no longer // understands (it owns a new typeId and the nested layout). - Hive.registerAdapter(SpiLegacyAdapter()); + registerHiveLegacyAdapters(); await PrefStore.shared.init(); // Call this before accessing any store await SecureStoreProps.migrateLegacyPrefs(); @@ -240,12 +240,15 @@ Future _doDbMigrate() async { // `current` while copying, because a pre-v3 record only exists as a Hive // value and that is the one pass that reads one. The call stays for the // downgrade check it performs, and for the next step that does exist. - await SchemaVersion.migrate(const []); - - // Then the app-level fixups, which read records as `Spi`. - ServerStore.instance.migrateIds(); - // After the stores are up: it decides against `Stores.key`, not by shape. - ServerStore.instance.migrateIdentityFilePaths(); + await SchemaVersion.migrate(const [KvToTablesMigration()]); + + // No app-level fixups follow. `migrateIds` and `migrateIdentityFilePaths` + // both scanned every server on every launch to repair a record only an + // upgrading install can hold; both are part of `KvToTablesMigration` now, + // which is the one pass that sees that shape. Neither could have run after + // it in any case — an empty `Spi.id` has nowhere to live once the id is a + // primary key, and mapping the old private key ids would have turned an + // `IdentityFile` path into a null before anything could recognise it. // Pick up sync history written under the pre-v3 remote filename. Runs at // most once per remote and is best-effort — see `inheritLegacyRemote`. diff --git a/lib/view/page/private_key/edit.dart b/lib/view/page/private_key/edit.dart index 4985cbdf23..1ad71cda51 100644 --- a/lib/view/page/private_key/edit.dart +++ b/lib/view/page/private_key/edit.dart @@ -10,6 +10,7 @@ import 'package:server_box/core/utils/server.dart'; import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/provider/private_key.dart'; import 'package:server_box/data/res/misc.dart'; +import 'package:server_box/data/store/entity_store.dart'; import 'package:server_box/view/widget/page_columns.dart'; const _format = 'text/plain'; @@ -68,7 +69,7 @@ class _PrivateKeyEditPageState extends ConsumerState { super.initState(); final pki = this.pki; if (pki != null) { - _nameController.text = pki.id; + _nameController.text = pki.name; _keyController.text = pki.key; } else { Clipboard.getData(_format).then((value) { @@ -109,7 +110,7 @@ class _PrivateKeyEditPageState extends ConsumerState { title: libL10n.attention, child: Text( libL10n.askContinue( - '${libL10n.delete} ${l10n.privateKey}(${pki.id})', + '${libL10n.delete} ${l10n.privateKey}(${pki.name})', ), ), actions: Btn.ok(red: true).toList, @@ -279,19 +280,34 @@ class _PrivateKeyEditPageState extends ConsumerState { _loading.value = SizedLoading.medium; try { final decrypted = await Computer.shared.start(decryptPem, [key, pwd]); - final pki = PrivateKeyInfo(id: name, key: decrypted); + // The id of the record being edited: renaming a key must not detach the + // servers pointing at it, which is what happened when the two were one + // value. + final pki = PrivateKeyInfo( + id: this.pki?.id ?? ShortId.generate(), + name: name, + key: decrypted, + ); final originPki = this.pki; if (originPki != null) { await _notifier.update(originPki, pki); } else { await _notifier.add(pki); } + } on DuplicateNameException catch (e) { + // The name is unique in the schema, so this is where a collision is + // found. The page stays open on the name the user has to change. + Toast.error(l10n.nameAlreadyExistsFmt(e.name)); + return; } catch (e) { Toast.error(e.toString()); rethrow; } finally { - _loading.value = null; + // `decryptPem` runs on another isolate, so the page can be gone by the + // time this runs — and `_loading` is disposed with it. + if (mounted) _loading.value = null; } + if (!mounted) return; context.pop(); } } diff --git a/lib/view/page/private_key/list.dart b/lib/view/page/private_key/list.dart index 0868718cff..96356d9082 100644 --- a/lib/view/page/private_key/list.dart +++ b/lib/view/page/private_key/list.dart @@ -50,7 +50,7 @@ class _PrivateKeyListState extends ConsumerState Widget _buildKeyItem(PrivateKeyInfo item) { return ListTile( - title: Text(item.id), + title: Text(item.name), subtitle: Text(item.type ?? libL10n.unknown, style: UIs.textGrey), onTap: () => PrivateKeyEditPage.route.go( context, @@ -75,7 +75,8 @@ extension on _PrivateKeyListState { final idRsaFile = File(home.joinPath('.ssh/id_rsa')); if (!idRsaFile.existsSync()) return; final sysPk = PrivateKeyInfo( - id: 'system', + id: ShortId.generate(), + name: 'system', key: await idRsaFile.readAsString(), ); context.showRoundDialog( diff --git a/lib/view/page/server/edit/widget.dart b/lib/view/page/server/edit/widget.dart index 42a2ee4c0f..53573b7549 100644 --- a/lib/view/page/server/edit/widget.dart +++ b/lib/view/page/server/edit/widget.dart @@ -94,7 +94,9 @@ extension _Widgets on _ServerEditPageState { final item = pkis[index]; return ChoiceChipX( key: ValueKey(index), - label: item.id, + // The name, not the id: an id is generated now, so this + // chip showed the user a `ShortId`. + label: item.name, state: state, value: index, onSelected: (idx, on) { diff --git a/lib/view/page/snippet/edit.dart b/lib/view/page/snippet/edit.dart index e4c019ef86..4e22d1e4bc 100644 --- a/lib/view/page/snippet/edit.dart +++ b/lib/view/page/snippet/edit.dart @@ -7,6 +7,7 @@ import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/provider/server/all.dart'; import 'package:server_box/data/provider/snippet.dart'; +import 'package:server_box/data/store/entity_store.dart'; import 'package:server_box/view/page/ssh/snippet_run.dart'; import 'package:server_box/view/widget/page_columns.dart'; @@ -146,6 +147,9 @@ class _SnippetEditPageState extends ConsumerState { } final note = _noteController.text; return Snippet( + // The id of the record being edited, so a rename stays an update of one + // column. A new snippet gets one that outlives every rename after it. + id: widget.args?.snippet?.id ?? ShortId.generate(), name: name, script: script, tags: _tags.value.isEmpty ? null : _tags.value.toList(), @@ -159,11 +163,20 @@ class _SnippetEditPageState extends ConsumerState { if (snippet == null) return; final oldSnippet = widget.args?.snippet; final notifier = ref.read(snippetProvider.notifier); - if (oldSnippet != null) { - await notifier.update(oldSnippet, snippet); - } else { - await notifier.add(snippet); + try { + if (oldSnippet != null) { + await notifier.update(oldSnippet, snippet); + } else { + await notifier.add(snippet); + } + } on DuplicateNameException catch (e) { + // The name is unique in the schema rather than in whichever dialog last + // remembered to check, so this is where a collision is found. The page + // stays open on the name the user has to change. + Toast.error(l10n.nameAlreadyExistsFmt(e.name)); + return; } + if (!mounted) return; _leave(); } diff --git a/packages/fl_lib b/packages/fl_lib index 94cbe59994..8c00e43819 160000 --- a/packages/fl_lib +++ b/packages/fl_lib @@ -1 +1 @@ -Subproject commit 94cbe59994ae7ca2ea986e902f01df18c0fc15fe +Subproject commit 8c00e438196079a016b5b6dafdeae5e3b486fda7 diff --git a/pubspec.lock b/pubspec.lock index c86878aacd..971b095d75 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" charset: dependency: transitive description: @@ -224,6 +232,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -352,6 +368,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + drift: + dependency: "direct main" + description: + name: drift + sha256: "3a3f1f6f905037d7426e4c445854139fd6a3d592135f7c96d7931682b73d16f4" + url: "https://pub.dev" + source: hosted + version: "2.34.3" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: "9cfff1576b49725da0d32c040651a41ae195e8c4af8d8da301593e41d7abc2f7" + url: "https://pub.dev" + source: hosted + version: "2.34.0" dynamic_color: dependency: "direct main" description: @@ -1361,6 +1393,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" record_use: dependency: transitive description: @@ -1606,6 +1646,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.5.1" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "40bdddb306a727be9ce510bd2d2b9a6c9db6c586d846ef7b22e3990a2b24f02d" + url: "https://pub.dev" + source: hosted + version: "0.44.5" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ec890f4be4..36f1d7072d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,10 @@ dependencies: # only honoured for this package's own dependencies, and the Hive migration # opens the database from here too. sqlite3: ^3.5.1 + # Storage layer over the same encrypted connection: `NativeDatabase.opened` + # takes the `sqlite3` Database this app already opens and keys itself, so + # sqlite3mc and the build-hook bundling below are untouched. + drift: ^2.34.3 wakelock_plus: ^1.7.0 wake_on_lan: ^4.1.1+3 webdav_client_plus: ^2.0.0 @@ -81,6 +85,7 @@ dev_dependencies: flutter_native_splash: ^2.1.6 hive_ce_generator: ^1.11.0 build_runner: ^2.4.15 + drift_dev: ^2.34.0 flutter_lints: ^6.0.0 json_serializable: ^6.13.0 freezed: ^3.0.0 diff --git a/test/agent_conversation_store_test.dart b/test/agent_conversation_store_test.dart index 09228d8337..78951750ef 100644 --- a/test/agent_conversation_store_test.dart +++ b/test/agent_conversation_store_test.dart @@ -4,13 +4,14 @@ import 'package:server_box/data/model/ai/agent_conversation.dart'; import 'package:server_box/data/model/ai/ask_ai_models.dart'; import 'package:server_box/data/store/agent_conversation.dart'; +import 'helpers/test_db.dart'; + void main() { late AgentConversationStore store; setUp(() async { - SqliteDb.openInMemory(); + await openTestDb(); store = AgentConversationStore.forTest(); - store.init(); }); tearDown(SqliteDb.close); diff --git a/test/agent_session_test.dart b/test/agent_session_test.dart index 96707a3743..1c4ea4eaa5 100644 --- a/test/agent_session_test.dart +++ b/test/agent_session_test.dart @@ -13,6 +13,8 @@ import 'package:server_box/data/store/agent_conversation.dart'; import 'package:server_box/data/store/server.dart'; import 'package:server_box/data/store/setting.dart'; +import 'helpers/test_db.dart'; + void main() { const shellCommand = AskAiCommand( id: 'call-shell', @@ -233,11 +235,11 @@ void main() { setUpAll(() async { tempDir = await Directory.systemTemp.createTemp('server-box-agent-session-'); - SqliteDb.openInMemory(); + await openTestDb(); await getIt.reset(); getIt.registerSingleton(SettingStore.forTest()..init()); - getIt.registerSingleton(ServerStore.forTest()..init()); - conversationStore = AgentConversationStore.forTest()..init(); + getIt.registerSingleton(ServerStore.forTest()); + conversationStore = AgentConversationStore.forTest(); getIt.registerSingleton(conversationStore); }); diff --git a/test/agent_shell_view_test.dart b/test/agent_shell_view_test.dart index 44bb124359..3522a4f988 100644 --- a/test/agent_shell_view_test.dart +++ b/test/agent_shell_view_test.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -15,6 +13,8 @@ import 'package:server_box/generated/l10n/l10n.dart'; import 'package:server_box/view/page/agent/shell.dart'; import 'package:server_box/view/page/agent/view.dart'; +import 'helpers/test_db.dart'; + /// An [AgentSession] frozen at one state, as in `agent_view_test.dart`: these /// tests are about the window the conversation is shown in, not about the /// conversation. @@ -27,24 +27,21 @@ class _FixedSession extends AgentSession { void main() { TestWidgetsFlutterBinding.ensureInitialized(); - late Directory tempDir; setUp(() async { - tempDir = await Directory.systemTemp.createTemp('server-box-agent-shell-'); - SqliteDb.openInMemory(); + await openTestDb(); // In memory rather than on disk: the shell persists its mode the moment it // changes, so every test here writes, and none of them should leave a // database behind. getIt.registerSingleton(SettingStore.forTest()); getIt.registerSingleton( - AgentConversationStore.forTest()..init(), + AgentConversationStore.forTest(), ); }); tearDown(() async { await getIt.reset(); await SqliteDb.close(); - await tempDir.delete(recursive: true); }); /// Past the end of whatever is running. diff --git a/test/agent_view_test.dart b/test/agent_view_test.dart index c44cf6b2d8..de8b3d1bef 100644 --- a/test/agent_view_test.dart +++ b/test/agent_view_test.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -18,6 +16,8 @@ import 'package:server_box/view/page/agent/agent.dart'; import 'package:server_box/view/page/agent/history.dart'; import 'package:server_box/view/page/agent/view.dart'; +import 'helpers/test_db.dart'; + /// An [AgentSession] frozen at one state. /// /// The real one reaches for stored conversations in `build`; these tests are @@ -34,23 +34,20 @@ class _FixedSession extends AgentSession { void main() { TestWidgetsFlutterBinding.ensureInitialized(); - late Directory tempDir; setUp(() async { - tempDir = await Directory.systemTemp.createTemp('server-box-agent-view-'); - SqliteDb.openInMemory(); + await openTestDb(); // In memory: the return-key tests below write the setting they are about, // and none of them should leave a database behind. getIt.registerSingleton(SettingStore.forTest()); getIt.registerSingleton( - AgentConversationStore.forTest()..init(), + AgentConversationStore.forTest(), ); }); tearDown(() async { await getIt.reset(); await SqliteDb.close(); - await tempDir.delete(recursive: true); }); Future pump( diff --git a/test/backup_v2_test.dart b/test/backup_v2_test.dart index 8976a26c6a..94cd6a7aed 100644 --- a/test/backup_v2_test.dart +++ b/test/backup_v2_test.dart @@ -3,12 +3,55 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:server_box/data/model/app/bak/backup2.dart'; import 'package:server_box/data/model/app/tab.dart'; +import 'package:server_box/data/model/server/port_forward.dart'; import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/snippet.dart'; void main() { group('BackupV2 JSON encoding', () { + test('a typed port forward encodes, and comes back the same', () { + // `PortForwardConfig` is the one freezed model with no `.g.dart`, so its + // `toJson` is hand-written — and `_toEncodable` did not know the type at + // all, which threw the moment a backup carried one. + const forward = PortForwardConfig( + id: 'pf-1', + serverId: 'srv-1', + name: 'postgres', + type: PortForwardType.remote, + localHost: '127.0.0.1', + localPort: 15432, + remoteHost: '10.0.0.50', + remotePort: 5432, + ); + final backup = BackupV2( + version: BackupV2.formatVer, + date: 1, + spis: const {}, + snippets: const {}, + keys: const {}, + portForwards: const {'pf-1': forward}, + container: const {}, + history: const {}, + settings: const {}, + ); + + final encoded = backup.toJsonString(); + final decoded = json.decode(encoded) as Map; + final raw = (decoded['portForwards'] as Map)['pf-1'] as Map; + expect(raw['type'], 'remote', reason: 'the enum by name, not its index'); + expect(raw['remotePort'], 5432); + + // Through the reader the app actually uses, not just `fromJson`. + final reread = BackupV2.fromJsonString(encoded); + expect( + PortForwardConfig.fromJson( + Map.from(reread.portForwards['pf-1'] as Map), + ), + forward, + ); + }); + test('serializes typed store objects as JSON objects', () { final backup = BackupV2( version: BackupV2.formatVer, @@ -18,6 +61,7 @@ void main() { keys: { 'key': const PrivateKeyInfo( id: 'key', + name: 'key', key: '-----BEGIN OPENSSH PRIVATE KEY-----\nkey', ), }, @@ -130,7 +174,11 @@ final class _NotJsonEncodable {} final class _ThrowingPrivateKeyInfo extends PrivateKeyInfo { const _ThrowingPrivateKeyInfo() - : super(id: 'bad', key: '-----BEGIN OPENSSH PRIVATE KEY-----\nbad'); + : super( + id: 'bad', + name: 'bad', + key: '-----BEGIN OPENSSH PRIVATE KEY-----\nbad', + ); @override Map toJson() => throw StateError('broken toJson'); diff --git a/test/connection_stats_store_test.dart b/test/connection_stats_store_test.dart index 346ffba4bf..85f12ec61e 100644 --- a/test/connection_stats_store_test.dart +++ b/test/connection_stats_store_test.dart @@ -3,6 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:server_box/data/model/server/connection_stat.dart'; import 'package:server_box/data/store/connection_stats.dart'; +import 'helpers/test_db.dart'; + /// The bounds this table keeps itself inside, which used to be four hand-written /// passes over a K-V store and are now two `DELETE`s. void main() { @@ -21,10 +23,18 @@ void main() { ); setUp(() async { - SqliteDb.openInMemory(); + await openTestDb(); + // `server_id` is a foreign key now, so the servers these rows belong to + // have to exist — an attempt against a server that is gone has nothing to + // be a statistic of. + SqliteDb.instance.execute( + 'INSERT INTO server (id, name, ssh_ip) VALUES ' + "('a', 'a', '10.0.0.1'), ('b', 'b', '10.0.0.2'), " + "('s0', 's0', '10.0.1.0'), ('s1', 's1', '10.0.1.1'), " + "('s2', 's2', '10.0.1.2'), ('s3', 's3', '10.0.1.3');", + ); store = ConnectionStatsStore.instance; await store.init(); - await store.clearAll(); }); tearDown(SqliteDb.close); @@ -107,14 +117,24 @@ void main() { expect(kept.single.timestamp, base); }); - test('recording the same attempt twice does not double it', () async { + test('two attempts in the same millisecond are two rows', () async { + // They used to be one: the id was `_`, so the second + // attempt overwrote the first and the counters under-reported. The id is + // generated now, and this is what that buys. final at = base; await store.recordConnection(stat('a', at: at)); await store.recordConnection(stat('a', at: at, name: 'renamed')); final history = store.getConnectionHistory('a'); - expect(history.length, 1); - expect(history.single.serverName, 'renamed'); + expect(history.length, 2); + expect(history.map((e) => e.serverName), containsAll(['srv', 'renamed'])); + }); + + test('an attempt against a server that is gone is not recorded', () async { + // `server_id` is a foreign key. A status refresh can land after the user + // deleted the server, and the row would only ever be orphaned statistics. + await store.recordConnection(stat('deleted-server', at: base)); + expect(store.getConnectionHistory('deleted-server'), isEmpty); }); test('the summary counts both outcomes', () async { diff --git a/test/file_browser_test.dart b/test/file_browser_test.dart index b721b79d0b..e8b510a015 100644 --- a/test/file_browser_test.dart +++ b/test/file_browser_test.dart @@ -13,6 +13,7 @@ import 'package:server_box/data/store/setting.dart'; import 'package:server_box/generated/l10n/l10n.dart'; import 'package:server_box/view/page/storage/file_browser.dart'; +import 'helpers/test_db.dart'; /// A filesystem that is a map, so a browser test is about the browser. class _MapBackend implements FileBackend { @@ -98,7 +99,7 @@ void main() { setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-browser-'); - SqliteDb.openInMemory(); + await openTestDb(); getIt.registerSingleton(SettingStore.forTest()); }); diff --git a/test/file_tab_restore_test.dart b/test/file_tab_restore_test.dart index e21976729a..e2bc5b3bc5 100644 --- a/test/file_tab_restore_test.dart +++ b/test/file_tab_restore_test.dart @@ -14,6 +14,7 @@ import 'package:server_box/generated/l10n/l10n.dart'; import 'package:server_box/view/page/storage/tab.dart'; import 'helpers/spi_fixture.dart'; +import 'helpers/test_db.dart'; /// What the file tab reopens with. /// @@ -34,7 +35,7 @@ void main() { setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-filetab-'); - SqliteDb.openInMemory(); + await openTestDb(); // In memory: this tree writes as it builds, and a test has no // business leaving a database behind. getIt.registerSingleton(SettingStore.forTest()); diff --git a/test/file_transfer_test.dart b/test/file_transfer_test.dart index bcea61cff4..a652849d79 100644 --- a/test/file_transfer_test.dart +++ b/test/file_transfer_test.dart @@ -16,6 +16,7 @@ import 'package:server_box/data/store/server.dart'; import 'package:server_box/data/store/setting.dart'; import 'helpers/spi_fixture.dart'; +import 'helpers/test_db.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -48,7 +49,7 @@ void main() { setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-transfer-'); - SqliteDb.openInMemory(); + await openTestDb(); getIt.registerSingleton(SettingStore.forTest()); // Building an `SftpFileRef` reads keys and jump servers out of these, // which is the work these tests are checking happens on this side. diff --git a/test/fixtures/hive_v1466/README.md b/test/fixtures/README.md similarity index 54% rename from test/fixtures/hive_v1466/README.md rename to test/fixtures/README.md index 02a0ec917c..b862f131f4 100644 --- a/test/fixtures/hive_v1466/README.md +++ b/test/fixtures/README.md @@ -1,13 +1,31 @@ -# Hive boxes as v1.0.1466 wrote them +# Hive boxes as the released builds wrote them -Input for `test/hive_v1466_migration_test.dart`. Every file here was produced -by **that release's own generated adapters**, not by the current tree — which -is the whole point. A test that seeds its boxes through today's adapters can -only prove today's code is self-consistent; it cannot catch a decoder that -disagrees with what the old release actually put on disk. +Input for `test/hive_release_migration_test.dart`, which runs the whole upgrade +against them: `HiveImport` into `kv`, then `KvToTablesMigration` into the entity +tables. Asserting between the two would only prove the data reached a shape no +build ships. -v1.0.1466 is the build the App Store served while the SQLite migration was -written, so it is the version most installs upgrade from. +Every file here was produced by **that release's own generated adapters**, not +by the current tree — which is the whole point. The first run against the +tables migration found four field names the migration had guessed wrong, each +of which silently dropped an entire store, plus generated adapters that could +no longer open a released box at all. + +The SQLite layout has never shipped, so every install in the field is on Hive +and **every released version is a migration source**: + +| Fixture | Boxes | Notes | +| --- | --- | --- | +| `hive_v1466/` | 8 | The oldest still in the field, and what the App Store served while this was written | +| `hive_v1480/` | 8 | `hive_adapters.g.dart` is byte-identical to 1466; the generator ran unchanged | +| `hive_v1491/` | 9 | Adds `agent_conversation`, and new `setting` keys | + +1480 is kept even though its adapters match 1466: that they match is a fact +about today, checked by generating from the tag rather than assumed. + +A test that seeds its boxes through today's adapters can only prove today's +code is self-consistent; it cannot catch a decoder that disagrees with what +the old release actually put on disk. ## What is in it @@ -41,9 +59,9 @@ git submodule update --init packages/dartssh2 packages/circle_chart \ packages/xterm packages/watch_connectivity packages/plain_notification_token \ packages/fl_lib packages/fl_build flutter pub get -cp /test/fixtures/hive_v1466/gen_fixture.dart.txt test/gen_fixture.dart +cp /test/fixtures/hive_v/gen_fixture.dart.txt test/gen_fixture.dart flutter test test/gen_fixture.dart -cp /tmp/sb1466-out/*.hive /test/fixtures/hive_v1466/ +cp /tmp/sb-out/*.hive /test/fixtures/hive_v/ cd && git worktree remove /tmp/sb1466 ``` @@ -55,6 +73,11 @@ Two things bite when pointing this at a different tag: - **The encryption key.** The generator and the reading test must derive the same one. Both build it from `Random(1)`; keep that in step or the boxes will not open. +- **Build the records through that release's own models.** The 1491 agent + conversations were first hand-written as JSON and silently dropped on + import: the keys are snake_case and the item discriminator is `kind`, and + both guesses were wrong. A fixture written by hand tests the guess, not the + release. Do not regenerate these files to make a failing test pass. They are a record of what a shipped release wrote, and editing them to suit the current decoder diff --git a/test/fixtures/hive_v1480/conn_stats_index.hive b/test/fixtures/hive_v1480/conn_stats_index.hive new file mode 100644 index 0000000000..9f82ef85f0 --- /dev/null +++ b/test/fixtures/hive_v1480/conn_stats_index.hive @@ -0,0 +1,2 @@ +srv-pwd +srv-key diff --git a/test/fixtures/hive_v1480/connection_stats_enc.hive b/test/fixtures/hive_v1480/connection_stats_enc.hive new file mode 100644 index 0000000000..2df2ffeb52 Binary files /dev/null and b/test/fixtures/hive_v1480/connection_stats_enc.hive differ diff --git a/test/fixtures/hive_v1480/docker_enc.hive b/test/fixtures/hive_v1480/docker_enc.hive new file mode 100644 index 0000000000..eedd7de068 Binary files /dev/null and b/test/fixtures/hive_v1480/docker_enc.hive differ diff --git a/test/fixtures/hive_v1480/gen_fixture.dart.txt b/test/fixtures/hive_v1480/gen_fixture.dart.txt new file mode 100644 index 0000000000..25fb14bf7e --- /dev/null +++ b/test/fixtures/hive_v1480/gen_fixture.dart.txt @@ -0,0 +1,307 @@ +// Generates the Hive fixture that the current tree's migration test reads. +// +// Run from a worktree checked out at the tag whose on-disk shape is wanted: +// flutter test test/gen_fixture.dart +// It writes `/*_enc.hive`, which are copied into the main repo under +// `test/fixtures/hive_/`. +// +// The point is that these bytes are produced by that release's own adapters, +// not by a hand-copy of them into a test. +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:server_box/data/model/app/net_view.dart'; +import 'package:server_box/data/model/server/connection_stat.dart'; +import 'package:server_box/data/model/server/custom.dart'; +import 'package:server_box/data/model/server/port_forward.dart'; +import 'package:server_box/data/model/server/private_key_info.dart'; +import 'package:server_box/data/model/server/server_private_info.dart'; +import 'package:server_box/data/model/server/snippet.dart'; +import 'package:server_box/data/model/server/system.dart'; +import 'package:server_box/data/model/server/wol_cfg.dart'; +import 'package:server_box/hive/hive_registrar.g.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Where the boxes are written. Overridden with `--dart-define=OUT=...`. +const _outDir = String.fromEnvironment('OUT', defaultValue: '/tmp/sb1480-out'); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('generate', () async { + final out = Directory(_outDir); + if (out.existsSync()) out.deleteSync(recursive: true); + out.createSync(recursive: true); + Paths.doc = out.path; + // At this tag `HiveStore.init` asks path_provider for the documents + // directory on every platform but Linux/Windows, so the boxes would land + // outside `out` — or, in a test, not at all. + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('plugins.flutter.io/path_provider'), + (call) async => out.path, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('plugins.flutter.io/path_provider_macos'), + (call) async => out.path, + ); + + // The same key the reading test derives, so the fixture opens there. One + // seeded generator reused, matching how the reading test builds it. + final rng = Random(1); + FlutterSecureStorage.setMockInitialValues({ + 'hivePwd': base64UrlEncode( + Uint8List.fromList(List.generate(32, (_) => rng.nextInt(256))), + ), + }); + SharedPreferences.setMockInitialValues({}); + await PrefStore.shared.init(); + + Hive.init(out.path); + Hive.registerAdapters(); + + Future> open(String name) async { + final store = HiveStore(name); + await store.init(); + return store.box; + } + + // ---- server: every optional field exercised somewhere ---------------- + final server = await open('server'); + await server.put( + 'srv-pwd', + const Spi( + id: 'srv-pwd', + name: 'password auth', + ip: '10.0.0.1', + port: 22, + user: 'root', + pwd: 'hunter2', + tags: ['prod', 'db'], + autoConnect: true, + ), + ); + await server.put( + 'srv-key', + const Spi( + id: 'srv-key', + name: 'key auth + custom', + ip: '10.0.0.2', + port: 2222, + user: 'admin', + keyId: 'key-ed25519', + tags: ['staging'], + autoConnect: false, + alterUrl: 'admin@alt.example.com:2200', + envs: {'TERM': 'xterm-256color', 'LANG': 'en_US.UTF-8'}, + customSystemType: SystemType.linux, + disabledCmdTypes: ['sensors', 'smartctl'], + custom: ServerCustom( + pveAddr: 'https://pve.example.com:8006', + pveIgnoreCert: true, + cmds: {'uptime': 'uptime -p', 'who': 'w'}, + preferTempDev: 'coretemp', + tempIsCelsius: false, + logoUrl: 'https://example.com/logo.png', + netDev: 'eth0', + scriptDir: '/opt/sbm', + ), + wolCfg: WakeOnLanCfg( + mac: 'AA:BB:CC:DD:EE:FF', + ip: '10.0.0.255', + pwd: 'wolpwd', + ), + ), + ); + await server.put( + 'srv-jump', + const Spi( + id: 'srv-jump', + name: 'behind jump hosts', + ip: '192.168.1.10', + port: 22, + user: 'ops', + keyId: 'key-rsa', + jumpId: 'srv-pwd', + jumpIds: ['srv-pwd', 'srv-key'], + tags: ['internal'], + ), + ); + await server.put( + 'srv-proxy', + const Spi( + id: 'srv-proxy', + name: 'proxy command', + ip: 'unreachable.example', + port: 22, + user: 'tunnel', + proxyCommand: 'nc -X 5 -x 127.0.0.1:1080 %h %p', + ), + ); + // Minimum: nothing optional set, and an empty id, which `parseId` fills. + await server.put( + 'srv-bare', + const Spi(name: 'bare', ip: '10.0.0.9', port: 22, user: 'root'), + ); + + // ---- key -------------------------------------------------------------- + final key = await open('key'); + await key.put( + 'key-ed25519', + const PrivateKeyInfo( + id: 'key-ed25519', + key: '-----BEGIN OPENSSH PRIVATE KEY-----\nZmFrZQ==\n' + '-----END OPENSSH PRIVATE KEY-----', + ), + ); + await key.put( + 'key-rsa', + const PrivateKeyInfo( + id: 'key-rsa', + key: '-----BEGIN RSA PRIVATE KEY-----\nZmFrZQ==\n' + '-----END RSA PRIVATE KEY-----', + ), + ); + + // ---- snippet ---------------------------------------------------------- + final snippet = await open('snippet'); + await snippet.put( + 'uptime', + const Snippet(name: 'uptime', script: 'uptime -p'), + ); + await snippet.put( + 'deploy', + const Snippet( + name: 'deploy', + script: 'cd /srv/app && git pull && systemctl restart app', + tags: ['ops', 'risky'], + note: 'run on the app hosts only', + autoRunOn: ['srv-pwd', 'srv-key'], + ), + ); + // Multi-line with the characters most likely to survive badly. + await snippet.put( + 'unicode', + const Snippet( + name: '日本語 / emoji 🚀', + script: "echo '引号 \"双\" \\\\ 反斜杠'\nprintf '%s\\n' \$HOME", + note: 'ünïcödé', + ), + ); + + // ---- setting: one of every stored type -------------------------------- + final setting = await open('setting'); + await setting.put('timeOut', 9); // int + await setting.put('textFactor', 1.25); // double + await setting.put('recordHistory', false); // bool + await setting.put('locale', 'zh'); // String + await setting.put('primaryColor', 4287106639); // large int + await setting.put('homeTabs', ['server', 'ssh', 'snippet']); // List + await setting.put('sshVirtKeys', [0, 1, 2, 13, 14]); // List + await setting.put('serverOrder', ['srv-key', 'srv-pwd', 'srv-jump']); + await setting.put('detailCardDisabled', ['temperature']); + await setting.put('netViewType', NetViewType.speed.name); // enum by name + await setting.put('sshKnownHostFingerprints', { + 'srv-pwd::ssh-ed25519': 'SHA256:AAAA', + 'srv-key::ssh-rsa': 'SHA256:BBBB', + }); // Map + await setting.put('termFontSize', 13.0); + await setting.put('maxRetryCount', 2); + await setting.put('useBioAuth', true); + + // ---- history ---------------------------------------------------------- + final history = await open('history'); + await history.put('sftpPath', ['/etc', '/var/log', '/home/ops']); + await history.put('sshTabs', '[]'); + + // ---- docker ----------------------------------------------------------- + final docker = await open('docker'); + // `containerHost${type.name}${id}` — the key `ContainerStore` builds. + await docker.put('containerHostdocker', 'unix:///var/run/docker.sock'); + await docker.put('containerHostdockersrv-key', 'tcp://10.0.0.2:2375'); + await docker.put('containerHostpodmansrv-jump', 'unix:///run/podman.sock'); + + // ---- port_forward ----------------------------------------------------- + final portForward = await open('port_forward'); + await portForward.put( + 'pf-local', + const PortForwardConfig( + id: 'pf-local', + serverId: 'srv-pwd', + name: 'postgres', + type: PortForwardType.local, + localHost: '127.0.0.1', + localPort: 15432, + remoteHost: '10.0.0.50', + remotePort: 5432, + ), + ); + await portForward.put( + 'pf-remote', + const PortForwardConfig( + id: 'pf-remote', + serverId: 'srv-key', + name: 'webhook', + type: PortForwardType.remote, + localHost: '127.0.0.1', + localPort: 8080, + remoteHost: '0.0.0.0', + remotePort: 9000, + ), + ); + + // ---- connection_stats: enough rows to cross the per-server cap -------- + final stats = await open('connection_stats'); + final base = DateTime.utc(2026, 1, 1, 12); + var n = 0; + for (final (serverId, serverName) in const [ + ('srv-pwd', 'password auth'), + ('srv-key', 'key auth + custom'), + ]) { + for (var i = 0; i < 60; i++) { + final ts = base.add(Duration(minutes: n++)); + final result = switch (i % 5) { + 0 => ConnectionResult.timeout, + 1 => ConnectionResult.authFailed, + 2 => ConnectionResult.networkError, + 3 => ConnectionResult.unknownError, + _ => ConnectionResult.success, + }; + await stats.put( + '${serverId}_${ts.millisecondsSinceEpoch}', + ConnectionStat( + serverId: serverId, + serverName: serverName, + timestamp: ts, + result: result, + errorMessage: result == ConnectionResult.success ? '' : 'boom $i', + durationMs: 10 + i, + ), + ); + } + } + + await Hive.close(); + + // The one box the app wrote without a cipher. + File(out.path.joinPath('conn_stats_index.hive')) + .writeAsStringSync('srv-pwd\nsrv-key\n'); + + final written = out + .listSync() + .whereType() + .map((f) => f.path.split('/').last) + .toList() + ..sort(); + // ignore: avoid_print + print('WROTE ${written.length} files into ${out.path}: $written'); + }); +} diff --git a/test/fixtures/hive_v1480/history_enc.hive b/test/fixtures/hive_v1480/history_enc.hive new file mode 100644 index 0000000000..9c8e23b50c Binary files /dev/null and b/test/fixtures/hive_v1480/history_enc.hive differ diff --git a/test/fixtures/hive_v1480/key_enc.hive b/test/fixtures/hive_v1480/key_enc.hive new file mode 100644 index 0000000000..d3bf82b0f0 Binary files /dev/null and b/test/fixtures/hive_v1480/key_enc.hive differ diff --git a/test/fixtures/hive_v1480/port_forward_enc.hive b/test/fixtures/hive_v1480/port_forward_enc.hive new file mode 100644 index 0000000000..b6c0af6a96 Binary files /dev/null and b/test/fixtures/hive_v1480/port_forward_enc.hive differ diff --git a/test/fixtures/hive_v1480/server_enc.hive b/test/fixtures/hive_v1480/server_enc.hive new file mode 100644 index 0000000000..9044a1f879 Binary files /dev/null and b/test/fixtures/hive_v1480/server_enc.hive differ diff --git a/test/fixtures/hive_v1480/setting_enc.hive b/test/fixtures/hive_v1480/setting_enc.hive new file mode 100644 index 0000000000..3844f1a42b Binary files /dev/null and b/test/fixtures/hive_v1480/setting_enc.hive differ diff --git a/test/fixtures/hive_v1480/snippet_enc.hive b/test/fixtures/hive_v1480/snippet_enc.hive new file mode 100644 index 0000000000..bdc65621d3 Binary files /dev/null and b/test/fixtures/hive_v1480/snippet_enc.hive differ diff --git a/test/fixtures/hive_v1491/agent_conversation_enc.hive b/test/fixtures/hive_v1491/agent_conversation_enc.hive new file mode 100644 index 0000000000..1b9b5bf0b9 Binary files /dev/null and b/test/fixtures/hive_v1491/agent_conversation_enc.hive differ diff --git a/test/fixtures/hive_v1491/conn_stats_index.hive b/test/fixtures/hive_v1491/conn_stats_index.hive new file mode 100644 index 0000000000..9f82ef85f0 --- /dev/null +++ b/test/fixtures/hive_v1491/conn_stats_index.hive @@ -0,0 +1,2 @@ +srv-pwd +srv-key diff --git a/test/fixtures/hive_v1491/connection_stats_enc.hive b/test/fixtures/hive_v1491/connection_stats_enc.hive new file mode 100644 index 0000000000..862cdecaba Binary files /dev/null and b/test/fixtures/hive_v1491/connection_stats_enc.hive differ diff --git a/test/fixtures/hive_v1491/docker_enc.hive b/test/fixtures/hive_v1491/docker_enc.hive new file mode 100644 index 0000000000..faf4200301 Binary files /dev/null and b/test/fixtures/hive_v1491/docker_enc.hive differ diff --git a/test/fixtures/hive_v1491/gen_fixture.dart.txt b/test/fixtures/hive_v1491/gen_fixture.dart.txt new file mode 100644 index 0000000000..3007283860 --- /dev/null +++ b/test/fixtures/hive_v1491/gen_fixture.dart.txt @@ -0,0 +1,346 @@ +// Generates the Hive fixture that the current tree's migration test reads. +// +// Run from a worktree checked out at the tag whose on-disk shape is wanted: +// flutter test test/gen_fixture.dart +// It writes `/*_enc.hive`, which are copied into the main repo under +// `test/fixtures/hive_/`. +// +// The point is that these bytes are produced by that release's own adapters, +// not by a hand-copy of them into a test. +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:server_box/data/model/ai/agent_conversation.dart'; +import 'package:server_box/data/model/ai/ask_ai_models.dart'; +import 'package:server_box/data/model/app/net_view.dart'; +import 'package:server_box/data/model/server/connection_stat.dart'; +import 'package:server_box/data/model/server/custom.dart'; +import 'package:server_box/data/model/server/port_forward.dart'; +import 'package:server_box/data/model/server/private_key_info.dart'; +import 'package:server_box/data/model/server/server_private_info.dart'; +import 'package:server_box/data/model/server/snippet.dart'; +import 'package:server_box/data/model/server/system.dart'; +import 'package:server_box/data/model/server/wol_cfg.dart'; +import 'package:server_box/hive/hive_registrar.g.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Where the boxes are written. Overridden with `--dart-define=OUT=...`. +const _outDir = String.fromEnvironment('OUT', defaultValue: '/tmp/sb1491-out'); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('generate', () async { + final out = Directory(_outDir); + if (out.existsSync()) out.deleteSync(recursive: true); + out.createSync(recursive: true); + Paths.doc = out.path; + // At this tag `HiveStore.init` asks path_provider for the documents + // directory on every platform but Linux/Windows, so the boxes would land + // outside `out` — or, in a test, not at all. + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('plugins.flutter.io/path_provider'), + (call) async => out.path, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('plugins.flutter.io/path_provider_macos'), + (call) async => out.path, + ); + + // The same key the reading test derives, so the fixture opens there. One + // seeded generator reused, matching how the reading test builds it. + final rng = Random(1); + FlutterSecureStorage.setMockInitialValues({ + 'hivePwd': base64UrlEncode( + Uint8List.fromList(List.generate(32, (_) => rng.nextInt(256))), + ), + }); + SharedPreferences.setMockInitialValues({}); + await PrefStore.shared.init(); + + Hive.init(out.path); + Hive.registerAdapters(); + + Future> open(String name) async { + final store = HiveStore(name); + await store.init(); + return store.box; + } + + // ---- server: every optional field exercised somewhere ---------------- + final server = await open('server'); + await server.put( + 'srv-pwd', + const Spi( + id: 'srv-pwd', + name: 'password auth', + ip: '10.0.0.1', + port: 22, + user: 'root', + pwd: 'hunter2', + tags: ['prod', 'db'], + autoConnect: true, + ), + ); + await server.put( + 'srv-key', + const Spi( + id: 'srv-key', + name: 'key auth + custom', + ip: '10.0.0.2', + port: 2222, + user: 'admin', + keyId: 'key-ed25519', + tags: ['staging'], + autoConnect: false, + alterUrl: 'admin@alt.example.com:2200', + envs: {'TERM': 'xterm-256color', 'LANG': 'en_US.UTF-8'}, + customSystemType: SystemType.linux, + disabledCmdTypes: ['sensors', 'smartctl'], + custom: ServerCustom( + pveAddr: 'https://pve.example.com:8006', + pveIgnoreCert: true, + cmds: {'uptime': 'uptime -p', 'who': 'w'}, + preferTempDev: 'coretemp', + tempIsCelsius: false, + logoUrl: 'https://example.com/logo.png', + netDev: 'eth0', + scriptDir: '/opt/sbm', + ), + wolCfg: WakeOnLanCfg( + mac: 'AA:BB:CC:DD:EE:FF', + ip: '10.0.0.255', + pwd: 'wolpwd', + ), + ), + ); + await server.put( + 'srv-jump', + const Spi( + id: 'srv-jump', + name: 'behind jump hosts', + ip: '192.168.1.10', + port: 22, + user: 'ops', + keyId: 'key-rsa', + jumpId: 'srv-pwd', + jumpIds: ['srv-pwd', 'srv-key'], + tags: ['internal'], + ), + ); + await server.put( + 'srv-proxy', + const Spi( + id: 'srv-proxy', + name: 'proxy command', + ip: 'unreachable.example', + port: 22, + user: 'tunnel', + proxyCommand: 'nc -X 5 -x 127.0.0.1:1080 %h %p', + ), + ); + // Minimum: nothing optional set, and an empty id, which `parseId` fills. + await server.put( + 'srv-bare', + const Spi(name: 'bare', ip: '10.0.0.9', port: 22, user: 'root'), + ); + + // ---- key -------------------------------------------------------------- + final key = await open('key'); + await key.put( + 'key-ed25519', + const PrivateKeyInfo( + id: 'key-ed25519', + key: '-----BEGIN OPENSSH PRIVATE KEY-----\nZmFrZQ==\n' + '-----END OPENSSH PRIVATE KEY-----', + ), + ); + await key.put( + 'key-rsa', + const PrivateKeyInfo( + id: 'key-rsa', + key: '-----BEGIN RSA PRIVATE KEY-----\nZmFrZQ==\n' + '-----END RSA PRIVATE KEY-----', + ), + ); + + // ---- snippet ---------------------------------------------------------- + final snippet = await open('snippet'); + await snippet.put( + 'uptime', + const Snippet(name: 'uptime', script: 'uptime -p'), + ); + await snippet.put( + 'deploy', + const Snippet( + name: 'deploy', + script: 'cd /srv/app && git pull && systemctl restart app', + tags: ['ops', 'risky'], + note: 'run on the app hosts only', + autoRunOn: ['srv-pwd', 'srv-key'], + ), + ); + // Multi-line with the characters most likely to survive badly. + await snippet.put( + 'unicode', + const Snippet( + name: '日本語 / emoji 🚀', + script: "echo '引号 \"双\" \\\\ 反斜杠'\nprintf '%s\\n' \$HOME", + note: 'ünïcödé', + ), + ); + + // ---- setting: one of every stored type -------------------------------- + final setting = await open('setting'); + await setting.put('timeOut', 9); // int + await setting.put('textFactor', 1.25); // double + await setting.put('recordHistory', false); // bool + await setting.put('locale', 'zh'); // String + await setting.put('primaryColor', 4287106639); // large int + await setting.put('homeTabs', ['server', 'ssh', 'snippet']); // List + await setting.put('sshVirtKeys', [0, 1, 2, 13, 14]); // List + await setting.put('serverOrder', ['srv-key', 'srv-pwd', 'srv-jump']); + await setting.put('detailCardDisabled', ['temperature']); + await setting.put('netViewType', NetViewType.speed.name); // enum by name + await setting.put('sshKnownHostFingerprints', { + 'srv-pwd::ssh-ed25519': 'SHA256:AAAA', + 'srv-key::ssh-rsa': 'SHA256:BBBB', + }); // Map + await setting.put('termFontSize', 13.0); + await setting.put('maxRetryCount', 2); + await setting.put('useBioAuth', true); + + // ---- history ---------------------------------------------------------- + final history = await open('history'); + await history.put('sftpPath', ['/etc', '/var/log', '/home/ops']); + await history.put('sshTabs', '[]'); + + // ---- docker ----------------------------------------------------------- + final docker = await open('docker'); + // `containerHost${type.name}${id}` — the key `ContainerStore` builds. + await docker.put('containerHostdocker', 'unix:///var/run/docker.sock'); + await docker.put('containerHostdockersrv-key', 'tcp://10.0.0.2:2375'); + await docker.put('containerHostpodmansrv-jump', 'unix:///run/podman.sock'); + + // ---- port_forward ----------------------------------------------------- + final portForward = await open('port_forward'); + await portForward.put( + 'pf-local', + const PortForwardConfig( + id: 'pf-local', + serverId: 'srv-pwd', + name: 'postgres', + type: PortForwardType.local, + localHost: '127.0.0.1', + localPort: 15432, + remoteHost: '10.0.0.50', + remotePort: 5432, + ), + ); + await portForward.put( + 'pf-remote', + const PortForwardConfig( + id: 'pf-remote', + serverId: 'srv-key', + name: 'webhook', + type: PortForwardType.remote, + localHost: '127.0.0.1', + localPort: 8080, + remoteHost: '0.0.0.0', + remotePort: 9000, + ), + ); + + // ---- connection_stats: enough rows to cross the per-server cap -------- + final stats = await open('connection_stats'); + final base = DateTime.utc(2026, 1, 1, 12); + var n = 0; + for (final (serverId, serverName) in const [ + ('srv-pwd', 'password auth'), + ('srv-key', 'key auth + custom'), + ]) { + for (var i = 0; i < 60; i++) { + final ts = base.add(Duration(minutes: n++)); + final result = switch (i % 5) { + 0 => ConnectionResult.timeout, + 1 => ConnectionResult.authFailed, + 2 => ConnectionResult.networkError, + 3 => ConnectionResult.unknownError, + _ => ConnectionResult.success, + }; + await stats.put( + '${serverId}_${ts.millisecondsSinceEpoch}', + ConnectionStat( + serverId: serverId, + serverName: serverName, + timestamp: ts, + result: result, + errorMessage: result == ConnectionResult.success ? '' : 'boom $i', + durationMs: 10 + i, + ), + ); + } + } + + // ---- agent_conversation (new in v1.0.1491) --------------------------- + // Built through the release's own model and serialiser, not hand-written + // JSON: the keys are snake_case and the item discriminator is `kind`, and + // guessing either wrong produces a fixture that proves nothing. + final agent = await open('agent_conversation'); + final conv = AgentConversation( + id: 'conv-1', + serverId: 'srv-key', + title: '磁盘快满了', + createdAt: DateTime.utc(2026, 1, 2, 9), + updatedAt: DateTime.utc(2026, 1, 2, 9, 30), + protocol: AskAiProtocol.chatCompletions, + providerBaseUrl: 'https://api.example.com/v1', + model: 'gpt-4o-mini', + items: const [ + AskAiMessageItem(role: AskAiMessageRole.user, content: 'df -h'), + AskAiMessageItem( + role: AskAiMessageRole.assistant, + content: '/ is at 91%', + ), + ], + ); + await agent.put('conversation::${conv.id}', conv.toJson()); + final empty = AgentConversation( + id: 'conv-2', + serverId: 'srv-pwd', + title: '', + createdAt: DateTime.utc(2026, 1, 3, 8), + updatedAt: DateTime.utc(2026, 1, 3, 8, 5), + protocol: AskAiProtocol.responses, + providerBaseUrl: 'https://api.example.com/v1', + model: 'gpt-4o-mini', + items: const [], + ); + await agent.put('conversation::${empty.id}', empty.toJson()); + await agent.put('active::srv-key', 'conv-1'); + + await Hive.close(); + + // The one box the app wrote without a cipher. + File(out.path.joinPath('conn_stats_index.hive')) + .writeAsStringSync('srv-pwd\nsrv-key\n'); + + final written = out + .listSync() + .whereType() + .map((f) => f.path.split('/').last) + .toList() + ..sort(); + // ignore: avoid_print + print('WROTE ${written.length} files into ${out.path}: $written'); + }); +} diff --git a/test/fixtures/hive_v1491/history_enc.hive b/test/fixtures/hive_v1491/history_enc.hive new file mode 100644 index 0000000000..0b9c6696f3 Binary files /dev/null and b/test/fixtures/hive_v1491/history_enc.hive differ diff --git a/test/fixtures/hive_v1491/key_enc.hive b/test/fixtures/hive_v1491/key_enc.hive new file mode 100644 index 0000000000..f4bb0672f1 Binary files /dev/null and b/test/fixtures/hive_v1491/key_enc.hive differ diff --git a/test/fixtures/hive_v1491/port_forward_enc.hive b/test/fixtures/hive_v1491/port_forward_enc.hive new file mode 100644 index 0000000000..8592cf83e8 Binary files /dev/null and b/test/fixtures/hive_v1491/port_forward_enc.hive differ diff --git a/test/fixtures/hive_v1491/server_enc.hive b/test/fixtures/hive_v1491/server_enc.hive new file mode 100644 index 0000000000..1219a058e6 Binary files /dev/null and b/test/fixtures/hive_v1491/server_enc.hive differ diff --git a/test/fixtures/hive_v1491/setting_enc.hive b/test/fixtures/hive_v1491/setting_enc.hive new file mode 100644 index 0000000000..9f8e76ad84 Binary files /dev/null and b/test/fixtures/hive_v1491/setting_enc.hive differ diff --git a/test/fixtures/hive_v1491/snippet_enc.hive b/test/fixtures/hive_v1491/snippet_enc.hive new file mode 100644 index 0000000000..cf70ba7568 Binary files /dev/null and b/test/fixtures/hive_v1491/snippet_enc.hive differ diff --git a/test/helpers/test_db.dart b/test/helpers/test_db.dart new file mode 100644 index 0000000000..ec8ac98225 --- /dev/null +++ b/test/helpers/test_db.dart @@ -0,0 +1,19 @@ +import 'package:fl_lib/fl_lib.dart'; +import 'package:server_box/data/store/tables.dart'; + +/// Opens a fresh in-memory database with the entity schema on it. +/// +/// What `Stores.init` does for the app, minus everything that needs a +/// Flutter binding. A test that writes through a store needs the tables, the +/// foreign keys — which are per connection, so a pragma set anywhere else +/// would not apply here — and no file on disk: a real file write started in a +/// `testWidgets` fake-async zone completes on a callback that zone never +/// pumps, which is how one such test hung a whole run. +Future openTestDb() async { + // The previous test closed its handle directly, so the cached `AppDb` from + // that connection has to go with it. + resetTables(); + SqliteDb.openInMemory(); + SqliteDb.instance.execute('PRAGMA foreign_keys = ON;'); + await createTables(SqliteDb.instance); +} diff --git a/test/hive_import_test.dart b/test/hive_import_test.dart index f5b76b92c7..655ffbd83a 100644 --- a/test/hive_import_test.dart +++ b/test/hive_import_test.dart @@ -7,15 +7,13 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive_ce/hive.dart'; -import 'package:server_box/data/model/container/type.dart'; import 'package:server_box/data/model/server/connection_stat.dart'; -import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; -import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/model/server/ssh_credential.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/schema.dart'; import 'package:server_box/hive/hive_registrar.g.dart'; +import 'package:server_box/hive/legacy_adapters.dart'; import 'package:server_box/hive/spi_legacy_adapter.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -50,7 +48,7 @@ void main() { Hive.init(tempDir.path); Hive.registerAdapters(); - Hive.registerAdapter(SpiLegacyAdapter()); + registerHiveLegacyAdapters(); }); tearDownAll(() async { @@ -93,15 +91,35 @@ void main() { ), ); + // Through the released layouts, not through today's models: `Snippet` and + // `PrivateKeyInfo` have each gained a field since, so writing one here + // would produce bytes no install has and prove nothing about the ones that + // do. The frozen types are read-only, hence the seed-only writers below. + Hive.registerAdapter(_V1SnippetWriter(), override: true); + Hive.registerAdapter(_V1PrivateKeyWriter(), override: true); + final snippet = await open('snippet'); - await snippet.box.put('uptime', const Snippet(name: 'uptime', script: 'w')); + await snippet.box.put( + 'uptime', + const LegacySnippetV1( + name: 'uptime', + script: 'w', + tags: null, + note: null, + autoRunOn: null, + ), + ); final key = await open('key'); await key.box.put( 'k1', - const PrivateKeyInfo(id: 'k1', key: 'PRIVATE'), + const LegacyPrivateKeyV1(id: 'k1', key: 'PRIVATE'), ); + // Back to the read-only decoders the app registers. + Hive.registerAdapter(LegacySnippetAdapter(), override: true); + Hive.registerAdapter(LegacyPrivateKeyAdapter(), override: true); + final setting = await open('setting'); await setting.box.put('timeOut', 9); await setting.box.put('recordHistory', false); @@ -132,33 +150,61 @@ void main() { await Hive.close(); } + /// One row of `kv`, decoded. + /// + /// The import's whole output is that table — taking a record apart into + /// columns is `KvToTablesMigration`'s job, and `hive_release_migration_test` + /// is what covers the two steps end to end. Reading through the stores here + /// would test the migration instead of the import. + Map? kvRow(String store, String key) { + final rows = SqliteDb.instance.select( + 'SELECT value FROM kv WHERE store = ? AND key = ?;', + [store, key], + ); + if (rows.isEmpty) return null; + return json.decode(rows.single['value'] as String) as Map; + } + test('every box lands in the store that replaced it', () async { await seedHive(); await Stores.init(); - final spi = Stores.server.fetchOneRaw('srv-1'); - expect(spi?.name, 'prod'); - expect(spi?.ssh?.ip, '10.0.0.1'); - expect(spi?.ssh?.port, 2222); - expect(spi?.tags, ['a']); + final spi = kvRow('server', 'srv-1')!; + expect(spi['name'], 'prod'); + expect((spi['ssh'] as Map)['ip'], '10.0.0.1'); + expect((spi['ssh'] as Map)['port'], 2222); + expect(spi['tags'], ['a']); - expect(Stores.snippet.fetchOneRaw('uptime')?.script, 'w'); - expect(Stores.key.fetchOneRaw('k1')?.key, 'PRIVATE'); + expect(kvRow('snippet', 'uptime')!['script'], 'w'); + // `private_key`, which is what the released model's `toJson` called it. + expect(kvRow('key', 'k1')!['private_key'], 'PRIVATE'); expect(Stores.setting.timeout.get(), 9); expect(Stores.setting.recordHistory.get(), false); expect(Stores.setting.homeTabs.get().map((e) => e.name), ['server', 'ssh']); expect(Stores.history.sftpGoPath.all, ['/etc', '/var']); + + final docker = SqliteDb.instance.select( + "SELECT key FROM kv WHERE store = 'docker';", + ); + expect(docker.map((r) => r['key']), contains('containerHostdocker')); + expect( - Stores.container.fetch('', ContainerType.docker), - 'unix:///var/run/docker.sock', + SqliteDb.instance + .select("SELECT count(*) AS n FROM kv WHERE store = 'conn_stat';") + .single['n'], + 1, ); expect( - Stores.connectionStats.getConnectionHistory('srv-1').single.serverName, - 'prod', + SqliteDb.instance + .select( + "SELECT value FROM kv WHERE store = 'agent_conversation' " + "AND key = 'active::srv-1';", + ) + .single['value'], + '"conv-1"', ); - expect(Stores.agentConversation.activeConversationId('srv-1'), 'conv-1'); }); test('the records are readable as JSON, not as adapter bytes', () async { @@ -176,10 +222,14 @@ void main() { expect((decoded['ssh'] as Map)['ip'], '10.0.0.1'); }); - test('it records the current schema version', () async { + test('it records the layout it wrote, not the current one', () async { await seedHive(); await Stores.init(); - expect(SchemaVersion.stored, SchemaVersion.current); + // The import produces the shape that was current when Hive was dropped. + // Claiming `current` would tell the migrator there is nothing left to do + // and strand every record in that shape. + expect(SchemaVersion.stored, SchemaVersion.hiveImportProduces); + expect(SchemaVersion.stored, lessThan(SchemaVersion.current)); }); test('a second launch does not import again', () async { @@ -212,21 +262,27 @@ void main() { ); }); - test('connection stats land in their table, not in kv', () async { + test('a connection stat arrives as the JSON that release wrote', () async { await seedHive(); await Stores.init(); - // The box held one row per attempt plus a second, unencrypted box of key - // lists. Both are one table now, so nothing of it should be left in `kv`. - final kv = SqliteDb.instance.select( - "SELECT count(*) AS n FROM kv WHERE store LIKE 'conn%';", - ).single['n']; - expect(kv, 0); - - final rows = SqliteDb.instance.select( - 'SELECT server_id FROM conn_stat;', + // The second, unencrypted box of key lists is not carried across at all: + // it held nothing the records do not already say. + expect( + SqliteDb.instance + .select( + "SELECT count(*) AS n FROM kv WHERE store = 'conn_stats_index';", + ) + .single['n'], + 0, ); - expect(rows.single['server_id'], 'srv-1'); + + final stat = kvRow('conn_stat', 'srv-1_1000')!; + expect(stat['serverId'], 'srv-1'); + expect(stat['serverName'], 'prod'); + // The `@JsonValue`, not the enum's name. Telling the two apart is the + // migration's job and it has a table for it. + expect(stat['result'], 'success'); }); test('a fresh install imports nothing and is already current', () async { @@ -234,7 +290,14 @@ void main() { await Stores.init(); expect(SchemaVersion.stored, SchemaVersion.current); - expect(Stores.server.fetch(), isEmpty); + // `setting` is not empty — the marker and the schema version live there — + // but no box contributed a record. + expect( + SqliteDb.instance + .select("SELECT count(*) AS n FROM kv WHERE store <> 'setting';") + .single['n'], + 0, + ); }); test('a box that could not be read is retried, the rest are not recopied', @@ -260,9 +323,9 @@ void main() { await Stores.init(); - expect(Stores.server.fetchOneRaw('srv-1')?.name, 'prod', + expect(kvRow('server', 'srv-1')?['name'], 'prod', reason: 'a box that opened is across'); - expect(Stores.snippet.fetchOneRaw('uptime'), isNull, + expect(kvRow('snippet', 'uptime'), isNull, reason: 'the box that did not open has nothing across'); // Stands in for a user edit between the two launches. The app is usable @@ -277,7 +340,7 @@ void main() { File(encPath).writeAsBytesSync(intact); await Stores.init(); - expect(Stores.snippet.fetchOneRaw('uptime')?.script, 'w', + expect(kvRow('snippet', 'uptime')?['script'], 'w', reason: 'the unread box is retried'); expect(Stores.setting.timeout.get(), 42, reason: 'a box already copied is not copied a second time'); @@ -285,16 +348,16 @@ void main() { test('a record the destination rejects does not hold its box open', () async { await seedHive(); - // `importRow` takes a map, so a String is rejected. Seeded here rather - // than in `seedHive`, which the other tests share. + // A value with no `toJson` and no JSON form. Seeded here rather than in + // `seedHive`, which the other tests share. final stats = HiveStore('connection_stats'); await stats.init(); - await stats.box.put('bad-row', 'not a record'); + await stats.box.put('bad-row', DateTime(2020)); await Hive.close(); await Stores.init(); - expect(Stores.connectionStats.getConnectionHistory('srv-1').length, 1, + expect(kvRow('conn_stat', 'srv-1_1000'), isNotNull, reason: 'the readable record still lands'); // Deliberate, and the opposite of an unopenable box: what makes a record @@ -357,9 +420,10 @@ void main() { await Stores.init(); - final spi = Stores.server.fetchOneRaw('srv-v2'); - expect(spi, isNotNull, reason: 'a typeId 3 record is still readable'); - expect(spi!.name, 'legacy'); + final nested = kvRow('server', 'srv-v2'); + expect(nested, isNotNull, reason: 'a typeId 3 record is still readable'); + final spi = Spi.fromJson(nested!); + expect(spi.name, 'legacy'); expect(spi.id, 'srv-v2'); expect(spi.tags, ['legacy']); expect(spi.autoConnect, false); @@ -457,3 +521,51 @@ class _V2SpiWriter extends TypeAdapter { ..write(ssh?.jumpIds); } } + +/// Writes the typeId 2 layout every released build wrote: no id, because a +/// snippet was keyed by its name. +class _V1SnippetWriter extends TypeAdapter { + @override + final typeId = 2; + + @override + LegacySnippetV1 read(BinaryReader reader) => + throw UnsupportedError('seed-only writer'); + + @override + void write(BinaryWriter writer, LegacySnippetV1 obj) { + writer + ..writeByte(5) + ..writeByte(0) + ..write(obj.name) + ..writeByte(1) + ..write(obj.script) + ..writeByte(2) + ..write(obj.tags) + ..writeByte(3) + ..write(obj.note) + ..writeByte(4) + ..write(obj.autoRunOn); + } +} + +/// Writes the typeId 1 layout every released build wrote: an id that was also +/// the name, and the key. +class _V1PrivateKeyWriter extends TypeAdapter { + @override + final typeId = 1; + + @override + LegacyPrivateKeyV1 read(BinaryReader reader) => + throw UnsupportedError('seed-only writer'); + + @override + void write(BinaryWriter writer, LegacyPrivateKeyV1 obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.key); + } +} diff --git a/test/hive_release_migration_test.dart b/test/hive_release_migration_test.dart new file mode 100644 index 0000000000..fd4a70663f --- /dev/null +++ b/test/hive_release_migration_test.dart @@ -0,0 +1,443 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:server_box/data/model/container/type.dart'; +import 'package:server_box/data/model/server/port_forward.dart'; +import 'package:server_box/data/model/server/system.dart'; +import 'package:server_box/data/res/store.dart'; +import 'package:server_box/data/store/migrations/m004_kv_to_tables.dart'; +import 'package:server_box/data/store/schema.dart'; +import 'package:server_box/hive/hive_registrar.g.dart'; +import 'package:server_box/hive/legacy_adapters.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// The upgrade path from every released build, against bytes those builds +/// wrote. +/// +/// The SQLite layout has never shipped, so **every** install in the field is +/// on Hive and any released version is a migration source. `hive_adapters.g +/// .dart` is byte-identical across 1466, 1480 and 1491, so the three share one +/// set of assertions — but 1491 also shipped an `agent_conversation` box, so +/// each gets a fixture of its own rather than the later ones being assumed +/// equal to the first. +/// +/// Seeding through the *current* adapters would only show that today's code +/// agrees with itself. These fixtures were produced by each release's own +/// generated adapters — see `test/fixtures/hive_v/README.md`. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const versions = ['1466', '1480', '1491']; + + late Directory tempDir; + + // Once for the process: `Paths.doc` is `late final`, so it cannot be set + // again for the second version. One directory, refilled per test instead. + setUpAll(() async { + tempDir = await Directory.systemTemp.createTemp('sbm-hive-releases-'); + Paths.doc = tempDir.path; + + final rng = Random(1); + FlutterSecureStorage.setMockInitialValues({ + 'hivePwd': base64UrlEncode( + Uint8List.fromList(List.generate(32, (_) => rng.nextInt(256))), + ), + }); + SharedPreferences.setMockInitialValues({}); + await PrefStore.shared.init(); + + Hive.init(tempDir.path); + Hive.registerAdapters(); + registerHiveLegacyAdapters(); + }); + + tearDownAll(() async { + await Hive.close(); + await tempDir.delete(recursive: true); + }); + + for (final version in versions) { + group('v1.0.$version', () { + final fixtureDir = Directory('test/fixtures/hive_v$version'); + + setUp(() async { + for (final f in tempDir.listSync()) { + if (f is File) f.deleteSync(); + } + for (final f in fixtureDir.listSync().whereType()) { + if (!f.path.endsWith('.hive')) continue; + f.copySync(tempDir.path.joinPath(f.path.split('/').last)); + } + SqliteDb.openInMemory(); + }); + + tearDown(() async { + await getIt.reset(); + await SqliteDb.close(); + await Hive.close(); + Hive.init(tempDir.path); + }); + + test('the fixture is present and encrypted', () { + expect(fixtureDir.existsSync(), isTrue); + final names = fixtureDir + .listSync() + .whereType() + .map((f) => f.path.split('/').last) + .where((n) => n.endsWith('.hive')) + .toList() + ..sort(); + expect(names, [ + // 1491 shipped one more box than the two before it. + if (version == '1491') 'agent_conversation_enc.hive', + 'conn_stats_index.hive', + 'connection_stats_enc.hive', + 'docker_enc.hive', + 'history_enc.hive', + 'key_enc.hive', + 'port_forward_enc.hive', + 'server_enc.hive', + 'setting_enc.hive', + 'snippet_enc.hive', + ]); + + // The one box 1466 wrote without a cipher, and the reason the others are + // worth keeping encrypted: this one is readable as text. + final index = File(fixtureDir.path.joinPath('conn_stats_index.hive')); + expect(index.readAsStringSync(), contains('srv-pwd')); + // An encrypted box is not. + final server = File(fixtureDir.path.joinPath('server_enc.hive')); + expect( + utf8.decode(server.readAsBytesSync(), allowMalformed: true), + isNot(contains('password auth')), + reason: 'the server name must not be readable in the file', + ); + }); + + /// The whole upgrade, not half of it: `HiveImport` leaves the records as + /// rows in `kv`, and `KvToTablesMigration` is what turns them into the + /// tables the app reads. Asserting between the two would only prove the + /// data reached an intermediate shape no build ever ships. + group('after importing the boxes and migrating', () { + setUp(() async { + await Stores.init(); + await SchemaVersion.migrate(const [KvToTablesMigration()]); + }); + + test('every server arrives with its SSH fields nested', () { + final ids = Stores.server.fetch().map((e) => e.id).toList(); + expect(ids.length, 5); + + final pwd = Stores.server.fetchOneRaw('srv-pwd')!; + expect(pwd.name, 'password auth'); + // Sorted, because a tag is a row and (server_id, tag) is its key. The + // order a tag was added in was never meaningful and is not kept. + expect(pwd.tags, ['db', 'prod']); + expect(pwd.autoConnect, isTrue); + expect(pwd.ssh?.ip, '10.0.0.1'); + expect(pwd.ssh?.port, 22); + expect(pwd.ssh?.user, 'root'); + expect(pwd.ssh?.pwd, 'hunter2'); + expect(pwd.ssh?.keyId, isNull); + // 1466 had no monitor support, so no record can carry one. + expect(pwd.monitorHttp, isNull); + }); + + test('the richest record keeps every optional field', () { + final spi = Stores.server.fetchOneRaw('srv-key')!; + expect(spi.name, 'key auth + custom'); + expect(spi.autoConnect, isFalse); + expect(spi.tags, ['staging']); + expect(spi.envs, {'TERM': 'xterm-256color', 'LANG': 'en_US.UTF-8'}); + expect(spi.customSystemType, SystemType.linux); + expect(spi.disabledCmdTypes, ['sensors', 'smartctl']); + + expect(spi.ssh?.ip, '10.0.0.2'); + expect(spi.ssh?.port, 2222); + expect(spi.ssh?.user, 'admin'); + // The key's id is generated now; its name is what the user typed. + expect(spi.ssh?.keyId, Stores.key.fetchByName('key-ed25519')!.id); + expect(spi.ssh?.pwd, isNull); + expect(spi.ssh?.alterUrl, 'admin@alt.example.com:2200'); + + final custom = spi.custom!; + expect(custom.pveAddr, 'https://pve.example.com:8006'); + expect(custom.pveIgnoreCert, isTrue); + expect(custom.cmds, {'uptime': 'uptime -p', 'who': 'w'}); + expect(custom.preferTempDev, 'coretemp'); + expect(custom.tempIsCelsius, isFalse); + expect(custom.logoUrl, 'https://example.com/logo.png'); + expect(custom.netDev, 'eth0'); + expect(custom.scriptDir, '/opt/sbm'); + + final wol = spi.wolCfg!; + expect(wol.mac, 'AA:BB:CC:DD:EE:FF'); + expect(wol.ip, '10.0.0.255'); + expect(wol.pwd, 'wolpwd'); + }); + + test('jump hosts and proxy command land on the credential', () { + final jump = Stores.server.fetchOneRaw('srv-jump')!; + expect(jump.ssh?.jumpId, 'srv-pwd'); + expect(jump.ssh?.jumpIds, ['srv-pwd', 'srv-key']); + expect(jump.ssh?.proxyCommand, isNull); + + final proxy = Stores.server.fetchOneRaw('srv-proxy')!; + expect(proxy.ssh?.proxyCommand, 'nc -X 5 -x 127.0.0.1:1080 %h %p'); + expect(proxy.ssh?.jumpIds, isNull); + }); + + test('a record with nothing optional set survives', () { + // 1466 wrote an empty id for a record predating them, so this one is + // found by name: an empty primary key is not a thing the table can + // hold, and the migration is where it is given a real id. + final bare = Stores.server.fetch().firstWhere((e) => e.name == 'bare'); + expect(bare.ssh?.ip, '10.0.0.9'); + expect(bare.custom, isNull); + expect(bare.wolCfg, isNull); + expect(bare.tags, isNull); + expect(bare.id, isNotEmpty); + }); + + test('the records are columns, and `kv` no longer holds them', () { + final row = SqliteDb.instance + .select( + 'SELECT ssh_ip, ssh_port, ssh_user, monitor_addr FROM server ' + 'WHERE id = ?;', + ['srv-key'], + ) + .single; + expect(row['ssh_ip'], '10.0.0.2'); + expect(row['ssh_port'], 2222); + expect(row['ssh_user'], 'admin'); + expect(row['monitor_addr'], isNull); + + for (final store in const ['server', 'key', 'snippet', 'docker']) { + expect( + SqliteDb.instance + .select('SELECT count(*) AS n FROM kv WHERE store = ?;', [store]) + .single['n'], + 0, + reason: 'a second copy of "$store" that a backup would carry', + ); + } + }); + + test('private keys and snippets come across whole', () { + expect(Stores.key.fetchByName('key-ed25519')?.key, + contains('BEGIN OPENSSH PRIVATE KEY')); + expect(Stores.key.fetchByName('key-rsa')?.key, + contains('BEGIN RSA PRIVATE KEY')); + + final deploy = Stores.snippet.fetchByName('deploy')!; + expect(deploy.script, contains('systemctl restart app')); + expect(deploy.tags, ['ops', 'risky']); + expect(deploy.note, 'run on the app hosts only'); + // Sorted, like tags: an auto-run target is a row keyed by the pair. + expect(deploy.autoRunOn, ['srv-key', 'srv-pwd']); + + // Non-ASCII, quotes, backslashes and newlines through Hive bytes and + // then through JSON and into a column. + final unicode = Stores.snippet.fetchByName('日本語 / emoji 🚀')!; + expect(unicode.note, 'ünïcödé'); + expect(unicode.script, contains(r'引号 "双" \\ 反斜杠')); + expect(unicode.script.split('\n').length, 2); + }); + + test('settings keep their type, not just their value', () { + expect(Stores.setting.timeout.get(), 9); + expect(Stores.setting.textFactor.get(), 1.25); + expect(Stores.setting.recordHistory.get(), isFalse); + expect(Stores.setting.useBioAuth.get(), isTrue); + expect(Stores.setting.locale.get(), 'zh'); + expect(Stores.setting.colorSeed.get(), 4287106639); + expect(Stores.setting.termFontSize.get(), 13.0); + expect(Stores.setting.maxRetryCount.get(), 2); + + expect(Stores.setting.homeTabs.get().map((e) => e.name), + ['server', 'ssh', 'snippet']); + expect(Stores.setting.sshVirtKeys.get(), [0, 1, 2, 13, 14]); + expect(Stores.setting.serverOrder.get(), + ['srv-key', 'srv-pwd', 'srv-jump']); + expect(Stores.setting.detailCardDisabled.get(), ['temperature']); + + // Stored by name in 1466 and still read by name — an index would have + // changed meaning silently. + expect(Stores.setting.netViewType.get().name, 'speed'); + + // Not a setting any more: a trusted fingerprint belongs to the server + // it was trusted for, and cascades with it. The old key is gone. + expect(Stores.setting.sshKnownHostFingerprints.get(), isEmpty); + expect(Stores.server.knownHosts('srv-pwd'), + {'ssh-ed25519': 'SHA256:AAAA'}); + expect(Stores.server.knownHosts('srv-key'), {'ssh-rsa': 'SHA256:BBBB'}); + }); + + test('history, container hosts and port forwards come across', () { + expect(Stores.history.sftpGoPath.all, ['/etc', '/var/log', '/home/ops']); + + // The global entry belonged to no server, and both container tables + // are children of one now — so it has nowhere to live and is dropped. + expect(Stores.container.fetch('', ContainerType.docker), isNull); + expect(Stores.container.fetch('srv-key', ContainerType.docker), + 'tcp://10.0.0.2:2375'); + expect(Stores.container.fetch('srv-jump', ContainerType.podman), + 'unix:///run/podman.sock'); + + final forwards = Stores.portForward.fetchForServer('srv-pwd'); + expect(forwards.length, 1); + expect(forwards.single.name, 'postgres'); + expect(forwards.single.type, PortForwardType.local); + expect(forwards.single.localPort, 15432); + expect(forwards.single.remoteHost, '10.0.0.50'); + expect(forwards.single.remotePort, 5432); + + expect(Stores.portForward.fetchForServer('srv-key').single.type, + PortForwardType.remote); + }); + + test('every connection stat row lands in its table', () { + // 60 per server, none dropped on the way in. The per-server cap is + // applied when recording, not when importing. + final rows = SqliteDb.instance + .select('SELECT count(*) AS n FROM conn_stat;') + .single['n']; + expect(rows, 120); + + final stats = Stores.connectionStats.getServerStats('srv-pwd', 'x'); + expect(stats.totalAttempts, 60); + // The generator made one in five a success. + expect(stats.successCount, 12); + expect(stats.failureCount, 48); + expect(stats.successRate, closeTo(0.2, 0.001)); + + // Every failure kind survives the round trip by name. + final kinds = SqliteDb.instance + .select('SELECT DISTINCT result FROM conn_stat ORDER BY result;') + .map((r) => r['result'] as String) + .toList(); + expect(kinds, [ + 'authFailed', + 'networkError', + 'success', + 'timeout', + 'unknownError', + ]); + + final one = Stores.connectionStats.getConnectionHistory('srv-key').first; + expect(one.serverName, 'key auth + custom'); + expect(one.durationMs, greaterThan(0)); + + // Every id is generated: the old `_` collided when + // two attempts landed in the same millisecond. + final ids = SqliteDb.instance + .select('SELECT id FROM conn_stat;') + .map((r) => r['id'] as String); + expect(ids.every((id) => !id.contains('_')), isTrue); + + // Nothing of the two stats boxes is left in `kv`. + expect( + SqliteDb.instance + .select("SELECT count(*) AS n FROM kv WHERE store LIKE 'conn%';") + .single['n'], + 0, + ); + }); + + test('the plaintext index is deleted and the rest kept', () { + expect( + File(tempDir.path.joinPath('conn_stats_index.hive')).existsSync(), + isFalse, + ); + expect( + File(tempDir.path.joinPath('server_enc.hive')).existsSync(), + isTrue, + reason: 'kept so a bad import can be rolled back to', + ); + }); + + test('the schema is up to date, and nothing reads as a local edit', () { + expect(SchemaVersion.stored, SchemaVersion.current); + // `Stores.lastModTime` decides which side of a sync wins; a device that + // has just read its own disk must not claim the newer copy. + expect(Stores.lastModTime, 0); + }); + + test('a second launch changes nothing', () async { + Stores.setting.timeout.put(42); + final before = Stores.server.fetchOneRaw('srv-key')!; + await getIt.reset(); + + await Stores.init(); + await SchemaVersion.migrate(const [KvToTablesMigration()]); + + expect(Stores.setting.timeout.get(), 42, reason: 'no re-import'); + expect(Stores.server.fetchOneRaw('srv-key')!.ssh?.ip, before.ssh?.ip); + expect(Stores.server.fetch().length, 5, reason: 'no duplicates'); + + // The stats are gone, and that is the retention policy rather than a + // loss: `ConnectionStatsStore.init` sweeps anything past 30 days, and + // the fixture is stamped in the past for good. On the importing launch + // they survive only because that sweep runs before `HiveImport`. + expect( + SqliteDb.instance + .select('SELECT count(*) AS n FROM conn_stat;') + .single['n'], + 0, + ); + }); + }); + /// What `m004_id_remap_test` seeds by hand, checked against the bytes a + /// release actually wrote. + /// + /// That test cannot take this fixture as its input — m004 consumes what + /// `HiveImport` leaves in `kv`, not Hive bytes — so the one fact it + /// assumes is pinned here instead: a server from before 1155 arrives + /// with an empty `id`, which is what makes the migration generate one. + test('a pre-1155 server reaches `kv` with no id of its own', () async { + // Import only. After m004 the row is gone from `kv` and the record has + // the id this asserts it did not have. + await Stores.init(); + + final rows = SqliteDb.instance.select( + "SELECT value FROM kv WHERE store = 'server';", + ); + final decoded = rows + .map((r) => json.decode(r['value'] as String) as Map) + .toList(); + final bare = decoded.firstWhere((e) => e['name'] == 'bare'); + expect(bare['id'], ''); + expect(bare['ssh'], isA()); + // And every other record does have one, so an empty id is the + // exception the remapping exists for rather than the norm. + expect( + decoded.where((e) => (e['id'] as String).isEmpty), + hasLength(1), + ); + }); + + test('the agent conversations come across when the release had them', () async { + await Stores.init(); + await SchemaVersion.migrate(const [KvToTablesMigration()]); + final had = version == '1491'; + final convs = Stores.agentConversation.fetchForServer('srv-key'); + expect(convs.length, had ? 1 : 0, + reason: had + ? '1491 shipped the box, so its rows must arrive' + : 'the box does not exist in this release'); + if (had) { + expect(convs.single.title, '磁盘快满了'); + expect(convs.single.items.length, 2); + expect(Stores.agentConversation.activeConversationId('srv-key'), + 'conv-1'); + } + }); + }); + } +} diff --git a/test/hive_v1466_migration_test.dart b/test/hive_v1466_migration_test.dart deleted file mode 100644 index 1d818f7d2f..0000000000 --- a/test/hive_v1466_migration_test.dart +++ /dev/null @@ -1,356 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:fl_lib/fl_lib.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; -import 'package:server_box/data/model/container/type.dart'; -import 'package:server_box/data/model/server/port_forward.dart'; -import 'package:server_box/data/model/server/system.dart'; -import 'package:server_box/data/res/store.dart'; -import 'package:server_box/data/store/schema.dart'; -import 'package:server_box/hive/hive_registrar.g.dart'; -import 'package:server_box/hive/spi_legacy_adapter.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -/// The upgrade path from a real released build, against bytes that build wrote. -/// -/// `hive_import_test.dart` seeds its boxes through the *current* adapters, -/// which is the wrong shape for this question: it cannot catch a decoder that -/// disagrees with what the old release actually put on disk. The fixture here -/// was produced by v1.0.1466's own generated adapters — see -/// `test/fixtures/hive_v1466/README.md` — so a mistake in reading it fails -/// here rather than on a user's device, where the pass is not repeatable. -/// -/// v1.0.1466 is the build the App Store served while this migration was -/// written, so it is the version most installs upgrade from. -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final fixtureDir = Directory('test/fixtures/hive_v1466'); - late Directory tempDir; - - setUpAll(() async { - tempDir = await Directory.systemTemp.createTemp('sbm-hive-v1466-'); - Paths.doc = tempDir.path; - - // The key the fixture was written with. Same construction as the - // generator, so the cipher matches and the boxes open. - final rng = Random(1); - FlutterSecureStorage.setMockInitialValues({ - 'hivePwd': base64UrlEncode( - Uint8List.fromList(List.generate(32, (_) => rng.nextInt(256))), - ), - }); - SharedPreferences.setMockInitialValues({}); - await PrefStore.shared.init(); - - Hive.init(tempDir.path); - Hive.registerAdapters(); - Hive.registerAdapter(SpiLegacyAdapter()); - }); - - tearDownAll(() async { - await Hive.close(); - await tempDir.delete(recursive: true); - }); - - setUp(() async { - // Fresh every time: the import deletes `conn_stats_index` and opening a - // box rewrites it, so a test must not run against what the last one left. - for (final f in tempDir.listSync()) { - if (f is File) f.deleteSync(); - } - for (final f in fixtureDir.listSync().whereType()) { - if (!f.path.endsWith('.hive')) continue; - f.copySync(tempDir.path.joinPath(f.path.split('/').last)); - } - SqliteDb.openInMemory(); - }); - - tearDown(() async { - await getIt.reset(); - await SqliteDb.close(); - await Hive.close(); - Hive.init(tempDir.path); - }); - - test('the fixture is present and encrypted', () { - expect(fixtureDir.existsSync(), isTrue); - final names = fixtureDir - .listSync() - .whereType() - .map((f) => f.path.split('/').last) - .where((n) => n.endsWith('.hive')) - .toList() - ..sort(); - expect(names, [ - 'conn_stats_index.hive', - 'connection_stats_enc.hive', - 'docker_enc.hive', - 'history_enc.hive', - 'key_enc.hive', - 'port_forward_enc.hive', - 'server_enc.hive', - 'setting_enc.hive', - 'snippet_enc.hive', - ]); - - // The one box 1466 wrote without a cipher, and the reason the others are - // worth keeping encrypted: this one is readable as text. - final index = File(fixtureDir.path.joinPath('conn_stats_index.hive')); - expect(index.readAsStringSync(), contains('srv-pwd')); - // An encrypted box is not. - final server = File(fixtureDir.path.joinPath('server_enc.hive')); - expect(server.readAsBytesSync(), isNot(contains(0x68656c))); // no plain run - expect( - utf8.decode(server.readAsBytesSync(), allowMalformed: true), - isNot(contains('password auth')), - reason: 'the server name must not be readable in the file', - ); - }); - - group('after importing the v1.0.1466 boxes', () { - setUp(() async => Stores.init()); - - test('every server arrives with its SSH fields nested', () { - final ids = Stores.server.fetch().map((e) => e.id).toList(); - expect(ids.length, 5); - - final pwd = Stores.server.fetchOneRaw('srv-pwd')!; - expect(pwd.name, 'password auth'); - expect(pwd.tags, ['prod', 'db']); - expect(pwd.autoConnect, isTrue); - expect(pwd.ssh?.ip, '10.0.0.1'); - expect(pwd.ssh?.port, 22); - expect(pwd.ssh?.user, 'root'); - expect(pwd.ssh?.pwd, 'hunter2'); - expect(pwd.ssh?.keyId, isNull); - // 1466 had no monitor support, so no record can carry one. - expect(pwd.monitorHttp, isNull); - }); - - test('the richest record keeps every optional field', () { - final spi = Stores.server.fetchOneRaw('srv-key')!; - expect(spi.name, 'key auth + custom'); - expect(spi.autoConnect, isFalse); - expect(spi.tags, ['staging']); - expect(spi.envs, {'TERM': 'xterm-256color', 'LANG': 'en_US.UTF-8'}); - expect(spi.customSystemType, SystemType.linux); - expect(spi.disabledCmdTypes, ['sensors', 'smartctl']); - - expect(spi.ssh?.ip, '10.0.0.2'); - expect(spi.ssh?.port, 2222); - expect(spi.ssh?.user, 'admin'); - expect(spi.ssh?.keyId, 'key-ed25519'); - expect(spi.ssh?.pwd, isNull); - expect(spi.ssh?.alterUrl, 'admin@alt.example.com:2200'); - - final custom = spi.custom!; - expect(custom.pveAddr, 'https://pve.example.com:8006'); - expect(custom.pveIgnoreCert, isTrue); - expect(custom.cmds, {'uptime': 'uptime -p', 'who': 'w'}); - expect(custom.preferTempDev, 'coretemp'); - expect(custom.tempIsCelsius, isFalse); - expect(custom.logoUrl, 'https://example.com/logo.png'); - expect(custom.netDev, 'eth0'); - expect(custom.scriptDir, '/opt/sbm'); - - final wol = spi.wolCfg!; - expect(wol.mac, 'AA:BB:CC:DD:EE:FF'); - expect(wol.ip, '10.0.0.255'); - expect(wol.pwd, 'wolpwd'); - }); - - test('jump hosts and proxy command land on the credential', () { - final jump = Stores.server.fetchOneRaw('srv-jump')!; - expect(jump.ssh?.jumpId, 'srv-pwd'); - expect(jump.ssh?.jumpIds, ['srv-pwd', 'srv-key']); - expect(jump.ssh?.proxyCommand, isNull); - - final proxy = Stores.server.fetchOneRaw('srv-proxy')!; - expect(proxy.ssh?.proxyCommand, 'nc -X 5 -x 127.0.0.1:1080 %h %p'); - expect(proxy.ssh?.jumpIds, isNull); - }); - - test('a record with nothing optional set survives', () { - final bare = Stores.server.fetchOneRaw('srv-bare')!; - expect(bare.name, 'bare'); - expect(bare.ssh?.ip, '10.0.0.9'); - expect(bare.custom, isNull); - expect(bare.wolCfg, isNull); - expect(bare.tags, isNull); - // 1466 wrote an empty id for a record predating them; `Spi.parseId` - // fills one in on read rather than leaving it blank. - expect(bare.id, isNotEmpty); - }); - - test('the stored JSON has no flat SSH key left', () { - final raw = SqliteDb.instance.select( - 'SELECT value FROM kv WHERE store = ? AND key = ?;', - ['server', 'srv-key'], - ).single['value'] as String; - final decoded = json.decode(raw) as Map; - expect((decoded['ssh'] as Map)['ip'], '10.0.0.2'); - for (final flat in const ['ip', 'port', 'user', 'pwd', 'pubKeyId']) { - expect(decoded.containsKey(flat), isFalse, reason: 'flat "$flat"'); - } - }); - - test('private keys and snippets come across whole', () { - expect(Stores.key.fetchOneRaw('key-ed25519')?.key, - contains('BEGIN OPENSSH PRIVATE KEY')); - expect(Stores.key.fetchOneRaw('key-rsa')?.key, - contains('BEGIN RSA PRIVATE KEY')); - - final deploy = Stores.snippet.fetchOneRaw('deploy')!; - expect(deploy.script, contains('systemctl restart app')); - expect(deploy.tags, ['ops', 'risky']); - expect(deploy.note, 'run on the app hosts only'); - expect(deploy.autoRunOn, ['srv-pwd', 'srv-key']); - - // Non-ASCII, quotes, backslashes and newlines through Hive bytes and - // then through JSON. - final unicode = Stores.snippet.fetchOneRaw('unicode')!; - expect(unicode.name, '日本語 / emoji 🚀'); - expect(unicode.note, 'ünïcödé'); - expect(unicode.script, contains(r'引号 "双" \\ 反斜杠')); - expect(unicode.script.split('\n').length, 2); - }); - - test('settings keep their type, not just their value', () { - expect(Stores.setting.timeout.get(), 9); - expect(Stores.setting.textFactor.get(), 1.25); - expect(Stores.setting.recordHistory.get(), isFalse); - expect(Stores.setting.useBioAuth.get(), isTrue); - expect(Stores.setting.locale.get(), 'zh'); - expect(Stores.setting.colorSeed.get(), 4287106639); - expect(Stores.setting.termFontSize.get(), 13.0); - expect(Stores.setting.maxRetryCount.get(), 2); - - expect(Stores.setting.homeTabs.get().map((e) => e.name), - ['server', 'ssh', 'snippet']); - expect(Stores.setting.sshVirtKeys.get(), [0, 1, 2, 13, 14]); - expect(Stores.setting.serverOrder.get(), - ['srv-key', 'srv-pwd', 'srv-jump']); - expect(Stores.setting.detailCardDisabled.get(), ['temperature']); - - // Stored by name in 1466 and still read by name — an index would have - // changed meaning silently. - expect(Stores.setting.netViewType.get().name, 'speed'); - - expect(Stores.setting.sshKnownHostFingerprints.get(), { - 'srv-pwd::ssh-ed25519': 'SHA256:AAAA', - 'srv-key::ssh-rsa': 'SHA256:BBBB', - }); - }); - - test('history, container hosts and port forwards come across', () { - expect(Stores.history.sftpGoPath.all, ['/etc', '/var/log', '/home/ops']); - - expect(Stores.container.fetch('', ContainerType.docker), - 'unix:///var/run/docker.sock'); - expect(Stores.container.fetch('srv-key', ContainerType.docker), - 'tcp://10.0.0.2:2375'); - expect(Stores.container.fetch('srv-jump', ContainerType.podman), - 'unix:///run/podman.sock'); - - final forwards = Stores.portForward.fetch('srv-pwd'); - expect(forwards.length, 1); - expect(forwards.single.name, 'postgres'); - expect(forwards.single.type, PortForwardType.local); - expect(forwards.single.localPort, 15432); - expect(forwards.single.remoteHost, '10.0.0.50'); - expect(forwards.single.remotePort, 5432); - - expect(Stores.portForward.fetch('srv-key').single.type, - PortForwardType.remote); - }); - - test('every connection stat row lands in its table', () { - // 60 per server, none dropped on the way in. The per-server cap is - // applied when recording, not when importing. - final rows = SqliteDb.instance - .select('SELECT count(*) AS n FROM conn_stat;') - .single['n']; - expect(rows, 120); - - final stats = Stores.connectionStats.getServerStats('srv-pwd', 'x'); - expect(stats.totalAttempts, 60); - // The generator made one in five a success. - expect(stats.successCount, 12); - expect(stats.failureCount, 48); - expect(stats.successRate, closeTo(0.2, 0.001)); - - // Every failure kind survives the round trip by name. - final kinds = SqliteDb.instance - .select('SELECT DISTINCT result FROM conn_stat ORDER BY result;') - .map((r) => r['result'] as String) - .toList(); - expect(kinds, [ - 'authFailed', - 'networkError', - 'success', - 'timeout', - 'unknownError', - ]); - - final one = Stores.connectionStats.getConnectionHistory('srv-key').first; - expect(one.serverName, 'key auth + custom'); - expect(one.durationMs, greaterThan(0)); - - // Nothing of the two stats boxes is left in `kv`. - expect( - SqliteDb.instance - .select("SELECT count(*) AS n FROM kv WHERE store LIKE 'conn%';") - .single['n'], - 0, - ); - }); - - test('the plaintext index is deleted and the rest kept', () { - expect( - File(tempDir.path.joinPath('conn_stats_index.hive')).existsSync(), - isFalse, - ); - expect( - File(tempDir.path.joinPath('server_enc.hive')).existsSync(), - isTrue, - reason: 'kept so a bad import can be rolled back to', - ); - }); - - test('the schema is marked current, and not as a local edit', () { - expect(SchemaVersion.stored, SchemaVersion.current); - // `Stores.lastModTime` decides which side of a sync wins; a device that - // has just read its own disk must not claim the newer copy. - expect(Stores.lastModTime, 0); - }); - - test('a second launch changes nothing', () async { - Stores.setting.timeout.put(42); - final before = Stores.server.fetchOneRaw('srv-key')!; - await getIt.reset(); - - await Stores.init(); - - expect(Stores.setting.timeout.get(), 42, reason: 'no re-import'); - expect(Stores.server.fetchOneRaw('srv-key')!.ssh?.ip, before.ssh?.ip); - expect(Stores.server.fetch().length, 5, reason: 'no duplicates'); - - // The stats are gone, and that is the retention policy rather than a - // loss: `ConnectionStatsStore.init` sweeps anything past 30 days, and - // the fixture is stamped in the past for good. On the importing launch - // they survive only because that sweep runs before `HiveImport`. - expect( - SqliteDb.instance - .select('SELECT count(*) AS n FROM conn_stat;') - .single['n'], - 0, - ); - }); - }); -} diff --git a/test/identity_file_key_test.dart b/test/identity_file_key_test.dart index aaad09bc7f..aceebbe338 100644 --- a/test/identity_file_key_test.dart +++ b/test/identity_file_key_test.dart @@ -12,19 +12,21 @@ library; import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:server_box/core/utils/server.dart'; import 'package:server_box/data/model/app/error.dart'; -import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/ssh_credential.dart'; +import 'package:server_box/data/store/migrations/m004_kv_to_tables.dart'; import 'package:server_box/data/store/private_key.dart'; import 'package:server_box/data/store/server.dart'; import 'helpers/spi_fixture.dart'; +import 'helpers/test_db.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -66,96 +68,125 @@ void main() { }); }); - group('migrateIdentityFilePaths', () { + /// The move used to be a scan of every server on every launch. It is part of + /// `KvToTablesMigration` now, and it had to be: that step remaps the private + /// key ids, so a `keyId` naming no key becomes a null there — and after that + /// there is nothing left to recognise as a path. + group('the key migration', () { late ServerStore servers; late PrivateKeyStore keys; - setUp(() { + setUp(() async { // In memory: this tree writes as it builds, and a test has no // business leaving a database behind. - SqliteDb.openInMemory(); + await openTestDb(); servers = ServerStore.forTest(); keys = PrivateKeyStore.forTest(); }); tearDown(SqliteDb.close); - /// Rows are JSON now, so a plain `get` would hand back the map. This - /// is the typed read. + /// One row of the shape m004 reads: the key-value layout m003 leaves. + void seed(String store, String key, Map value) { + SqliteDb.instance.execute( + 'INSERT INTO kv (store, key, value, updated_at) VALUES (?, ?, ?, 0);', + [store, key, json.encode(value)], + ); + } + + void seedServer(String id, {String? keyId, String? keyPath}) => seed( + 'server', + id, + { + 'id': id, + 'name': 'srv $id', + 'ssh': { + 'ip': '10.0.0.1', + 'port': 22, + 'user': 'root', + 'keyId': ?keyId, + 'keyPath': ?keyPath, + }, + }, + ); + + /// The old shape: the key's id *was* its name. + void seedKey(String name) => seed('key', name, {'id': name, 'key': 'PEM'}); + + Future migrate() => const KvToTablesMigration().apply(); + Spi reread(String id) => servers.fetchOneRaw(id)!; - test('a path that names no stored key moves to keyPath', () { - servers.put( - const Spi( - id: 'a', - name: 'imported', - ssh: SshCredential(ip: '10.0.0.1', keyId: '~/.ssh/id_ed25519'), - ), - ); + test('a path that names no stored key moves to keyPath', () async { + seedServer('a', keyId: '~/.ssh/id_ed25519'); - servers.migrateIdentityFilePaths(keys: keys); + await migrate(); final ssh = reread('a').ssh!; expect(ssh.keyPath, '~/.ssh/id_ed25519'); expect(ssh.keyId, isNull); }); - test('a key id that looks like a path is left alone', () { - // A key's id is whatever the user typed for its name, so this is a real + test('a key id that looks like a path is left alone', () async { + // A key's id was whatever the user typed for its name, so this is a real // key and not the import bug — and moving it would break a working server - keys.put(const PrivateKeyInfo(id: 'keys/prod', key: 'PEM')); - servers.put( - const Spi( - id: 'b', - name: 'named oddly', - ssh: SshCredential(ip: '10.0.0.2', keyId: 'keys/prod'), - ), - ); + seedKey('keys/prod'); + seedServer('b', keyId: 'keys/prod'); - servers.migrateIdentityFilePaths(keys: keys); + await migrate(); final ssh = reread('b').ssh!; - expect(ssh.keyId, 'keys/prod'); expect(ssh.keyPath, isNull); + // The id it points at is generated now, and it is the one the key got. + expect(ssh.keyId, keys.fetchByName('keys/prod')!.id); }); - test('an ordinary key id is untouched', () { - keys.put(const PrivateKeyInfo(id: 'work', key: 'PEM')); - servers.put( - const Spi( - id: 'c', - name: 'normal', - ssh: SshCredential(ip: '10.0.0.3', keyId: 'work'), - ), - ); + test('an ordinary key id follows the key to its new id', () async { + seedKey('work'); + seedServer('c', keyId: 'work'); - servers.migrateIdentityFilePaths(keys: keys); + await migrate(); - expect(reread('c').ssh!.keyId, 'work'); + final key = keys.fetchByName('work')!; + expect(key.id, isNot('work'), reason: 'an id is not a name'); + expect(reread('c').ssh!.keyId, key.id); expect(reread('c').ssh!.keyPath, isNull); }); - test('running it twice changes nothing further', () { - servers.put( - const Spi( - id: 'd', - name: 'imported', - ssh: SshCredential(ip: '10.0.0.4', keyId: '/abs/path/key'), - ), - ); + test('a keyPath already set is not overwritten', () async { + seedServer('d', keyPath: '/abs/path/key'); - servers.migrateIdentityFilePaths(keys: keys); - final once = reread('d'); - servers.migrateIdentityFilePaths(keys: keys); + await migrate(); - expect(reread('d'), once); + expect(reread('d').ssh!.keyPath, '/abs/path/key'); + expect(reread('d').ssh!.keyId, isNull); }); - test('a server with no SSH at all is skipped', () { - servers.put(const Spi(id: 'e', name: 'monitor only')); - servers.migrateIdentityFilePaths(keys: keys); + test('a monitor server has no SSH to look at', () async { + seed('server', 'e', { + 'id': 'e', + 'name': 'monitor only', + 'monitorHttp': {'addr': 'https://10.0.0.9:3770'}, + }); + + await migrate(); + expect(reread('e').ssh, isNull); }); + + test('two keys that shared a name both survive, under one each', () async { + // Nothing enforced that the name was unique across the two places a key + // could be created, and it is the primary key of the table now. + seed('key', 'dup', {'id': 'dup', 'key': 'FIRST'}); + seed('key', 'dup-2', {'id': 'dup', 'key': 'SECOND'}); + + await migrate(); + + final all = keys.fetch(); + expect(all.length, 2); + expect(all.map((e) => e.id).toSet().length, 2); + expect(all.map((e) => e.name).toSet(), {'dup', 'dup (2)'}); + }); }); group('resolvePrivateKey', () { diff --git a/test/m004_id_remap_test.dart b/test/m004_id_remap_test.dart new file mode 100644 index 0000000000..1f5b85ad89 --- /dev/null +++ b/test/m004_id_remap_test.dart @@ -0,0 +1,164 @@ +import 'dart:convert'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:server_box/data/model/container/type.dart'; +import 'package:server_box/data/store/agent_conversation.dart'; +import 'package:server_box/data/store/container.dart'; +import 'package:server_box/data/store/migrations/m004_kv_to_tables.dart'; +import 'package:server_box/data/store/port_forward.dart'; +import 'package:server_box/data/store/server.dart'; +import 'package:server_box/data/store/snippet.dart'; + +import 'helpers/test_db.dart'; + +/// A server stored before 1155 has an **empty** `id`, so an empty primary key +/// is what `KvToTablesMigration` would have to write — and it generates one +/// instead. Everything that named the old key has to follow it in the same +/// pass; nothing runs afterwards that could. +/// +/// The input here is hand-written because m004's input is not a release +/// artifact: it consumes what `HiveImport` leaves in `kv`, which is this +/// repo's own intermediate. +/// +/// Exactly two things about it are backed by a released build, both asserted +/// against the fixtures in `hive_release_migration_test.dart`: such a server +/// arrives with `id == ''`, and its `ssh` fields arrive nested under one key. +/// Those are what the seed cannot drift away from without that test failing. +/// +/// Nothing else here is: the `kv` key is an arbitrary legacy reference, chosen +/// to read like one rather than because any fixture verifies that shape. What +/// is under test is that the *reference* is rewritten, whatever it was. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late ServerStore servers; + + /// An arbitrary legacy reference: the `kv` key the server is stored under, + /// and the value every record below uses to name it. Its shape is not + /// asserted anywhere and nothing depends on it. + const legacyRef = 'root@10.0.0.1:22'; + + setUp(() async { + await openTestDb(); + servers = ServerStore.forTest(); + }); + + tearDown(SqliteDb.close); + + void seed(String store, String key, Object value) { + SqliteDb.instance.execute( + 'INSERT INTO kv (store, key, value, updated_at) VALUES (?, ?, ?, 0);', + [store, key, json.encode(value)], + ); + } + + /// The empty `id` and the nested `ssh` are the release-backed part; the key + /// it is stored under is not. + void seedLegacyServer() => seed('server', legacyRef, { + 'id': '', + 'name': 'legacy', + 'ssh': {'ip': '10.0.0.1', 'port': 22, 'user': 'root'}, + }); + + Future migrate() async { + await const KvToTablesMigration().apply(); + servers.dropCache(); + return servers.fetch().single.id; + } + + test('the server is given a real id', () async { + seedLegacyServer(); + + final id = await migrate(); + + expect(id, isNotEmpty); + expect(id, isNot(legacyRef), reason: 'an id is not a connection string'); + }); + + test('an agent conversation follows it, in the column and in the JSON', + () async { + // The store rebuilds a conversation from `data` and compares the + // `serverId` it finds there against the server it was asked about. A + // payload still naming the old key left the conversation reachable by no + // path at all: not `fetchActive`, not `setActive`, not delete. + seedLegacyServer(); + seed('agent_conversation', 'conversation::conv-1', { + 'id': 'conv-1', + 'server_id': legacyRef, + 'title': 'disk is filling up', + 'created_at': '2026-01-01T00:00:00.000', + 'updated_at': '2026-01-02T00:00:00.000', + 'protocol': 'responses', + 'provider_base_url': 'https://api.openai.com', + 'model': 'gpt-test', + 'items': [], + }); + seed('agent_conversation', 'active::$legacyRef', 'conv-1'); + + final id = await migrate(); + final store = AgentConversationStore.forTest(); + + expect(store.activeConversationId(id), 'conv-1'); + expect(store.fetchForServer(id).single.serverId, id); + // The one that reads the column and the payload together. + expect(store.fetchActive(id)?.id, 'conv-1'); + expect(store.fetchForServer(legacyRef), isEmpty); + }); + + test('a snippet auto-run target follows it', () async { + seedLegacyServer(); + seed('snippet', 'deploy', { + 'name': 'deploy', + 'script': 'systemctl restart app', + 'autoRunOn': [legacyRef], + }); + + final id = await migrate(); + + expect(SnippetStore.forTest().fetch().single.autoRunOn, [id]); + }); + + test('a port forward follows it', () async { + seedLegacyServer(); + seed('port_forward', 'pf-1', { + 'id': 'pf-1', + 'serverId': legacyRef, + 'name': 'postgres', + 'type': 'local', + 'localPort': 15432, + }); + + final id = await migrate(); + + expect(PortForwardStore.forTest().fetchForServer(id).single.id, 'pf-1'); + }); + + test('a container host follows it', () async { + seedLegacyServer(); + seed('docker', 'containerHostdocker$legacyRef', 'tcp://10.0.0.1:2375'); + + final id = await migrate(); + + expect( + ContainerStore.forTest().fetch(id, ContainerType.docker), + 'tcp://10.0.0.1:2375', + ); + }); + + test('the server order follows it', () async { + seedLegacyServer(); + seed('setting', 'serverOrder', [legacyRef]); + + final id = await migrate(); + + // Read out of `kv` rather than through `Stores.setting`, which would need + // the whole GetIt registration for one string. + final raw = SqliteDb.instance + .select("SELECT value FROM kv WHERE store = 'setting' AND key = ?;", [ + 'serverOrder', + ]) + .single['value'] as String; + expect(json.decode(raw), [id]); + }); +} diff --git a/test/pane_width_test.dart b/test/pane_width_test.dart index 63e624aeeb..270afb3d1b 100644 --- a/test/pane_width_test.dart +++ b/test/pane_width_test.dart @@ -14,6 +14,7 @@ import 'package:server_box/generated/l10n/l10n.dart'; import 'package:server_box/view/page/server/tab/tab.dart'; import 'helpers/spi_fixture.dart'; +import 'helpers/test_db.dart'; /// How narrow the list column may be, held to what it actually lays out at. /// @@ -35,7 +36,7 @@ void main() { setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-pane-'); - SqliteDb.openInMemory(); + await openTestDb(); // In memory: this tree writes as it builds, and a test has no // business leaving a database behind. getIt.registerSingleton(SettingStore.forTest()); diff --git a/test/port_forward_store_test.dart b/test/port_forward_store_test.dart index 231bc8b66f..fe04ba2580 100644 --- a/test/port_forward_store_test.dart +++ b/test/port_forward_store_test.dart @@ -3,6 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:server_box/data/model/server/port_forward.dart'; import 'package:server_box/data/store/port_forward.dart'; +import 'helpers/test_db.dart'; + /// A port forward has to survive being written. /// /// It did not, between the move to SQLite and this test: `SqliteStore` encodes @@ -13,8 +15,14 @@ import 'package:server_box/data/store/port_forward.dart'; void main() { late PortForwardStore store; - setUp(() { - SqliteDb.openInMemory(); + setUp(() async { + await openTestDb(); + // Both forwards below name a server, and `server_id` is a foreign key now. + SqliteDb.instance.execute( + 'INSERT INTO server (id, name, ssh_ip) VALUES ' + "('srv-1', 'one', '10.0.0.1'), ('srv-2', 'two', '10.0.0.2'), " + "('srv-other', 'other', '10.0.0.3');", + ); store = PortForwardStore.forTest(); }); @@ -34,7 +42,7 @@ void main() { test('a saved config is readable again', () { store.put(config); - final read = store.fetch('srv-1'); + final read = store.fetchForServer('srv-1'); expect(read.length, 1, reason: 'the write must not fail quietly'); expect(read.single, config); }); @@ -45,7 +53,7 @@ void main() { } final byType = { - for (final c in store.fetch('srv-1')) c.type: c, + for (final c in store.fetchForServer('srv-1')) c.type: c, }; expect(byType.keys.toSet(), PortForwardType.values.toSet()); }); @@ -59,7 +67,7 @@ void main() { ); store.put(minimal); - final read = store.fetch('srv-2').single; + final read = store.fetchForServer('srv-2').single; expect(read.localHost, isNull); expect(read.localPort, 0); expect(read.remoteHost, isNull); @@ -70,14 +78,14 @@ void main() { store.put(config); store.put(config.copyWith(id: 'pf-2', serverId: 'srv-other')); - expect(store.fetch('srv-1').single.id, 'pf-1'); - expect(store.fetch('srv-other').single.id, 'pf-2'); - expect(store.fetch('srv-none'), isEmpty); + expect(store.fetchForServer('srv-1').single.id, 'pf-1'); + expect(store.fetchForServer('srv-other').single.id, 'pf-2'); + expect(store.fetchForServer('srv-none'), isEmpty); }); test('delete removes it', () { store.put(config); store.delete(config); - expect(store.fetch('srv-1'), isEmpty); + expect(store.fetchForServer('srv-1'), isEmpty); }); } diff --git a/test/server_card_gesture_test.dart b/test/server_card_gesture_test.dart index 913e5d3e14..ba84a1e5c6 100644 --- a/test/server_card_gesture_test.dart +++ b/test/server_card_gesture_test.dart @@ -15,6 +15,7 @@ import 'package:server_box/view/page/server/edit/edit.dart'; import 'package:server_box/view/page/server/tab/tab.dart'; import 'helpers/spi_fixture.dart'; +import 'helpers/test_db.dart'; /// What a long press — and so a right-click — does to a server card. /// @@ -30,7 +31,7 @@ void main() { setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-card-'); - SqliteDb.openInMemory(); + await openTestDb(); // In memory: this tree writes as it builds, and a test has no // business leaving a database behind. getIt.registerSingleton(SettingStore.forTest()); diff --git a/test/server_edit_logic_test.dart b/test/server_edit_logic_test.dart index c5f4559703..445a4df2a9 100644 --- a/test/server_edit_logic_test.dart +++ b/test/server_edit_logic_test.dart @@ -143,7 +143,7 @@ void main() { port: 22, user: 'root', pwd: 'password123', - keyId: 'test-key', + keyId: 'key-id-1', id: 'combined-auth-server-id', ); Spi? persisted; @@ -159,7 +159,7 @@ void main() { ), privateKeyProvider.overrideWithValue( const PrivateKeyState( - keys: [PrivateKeyInfo(id: 'test-key', key: 'unused')], + keys: [PrivateKeyInfo(id: 'key-id-1', name: 'prod key', key: 'unused')], ), ), ], @@ -197,7 +197,8 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); - expect(find.text('test-key'), findsOneWidget); + // The name, not the id: an id is not something the user typed. + expect(find.text('prod key'), findsOneWidget); expect( tester .widgetList(find.byType(EditableText)) @@ -213,14 +214,16 @@ void main() { expect(persisted, isNotNull); final restored = persisted!; expect(restored.ssh?.pwd, 'password123'); - expect(restored.ssh?.keyId, 'test-key'); + // The reference is by id, which a rename cannot break. + expect(restored.ssh?.keyId, 'key-id-1'); server = restored; await tester.tap(find.byKey(const ValueKey('open-server-editor'))); await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); - expect(find.text('test-key'), findsOneWidget); + // The name, not the id: an id is not something the user typed. + expect(find.text('prod key'), findsOneWidget); expect( tester .widgetList(find.byType(EditableText)) diff --git a/test/server_func_btn_test.dart b/test/server_func_btn_test.dart index 65bdd9b457..74f7d67584 100644 --- a/test/server_func_btn_test.dart +++ b/test/server_func_btn_test.dart @@ -4,13 +4,15 @@ import 'package:server_box/data/model/app/menu/server_func.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/setting.dart'; +import 'helpers/test_db.dart'; + /// What `ServerFuncBtn.autoAddNewFuncs` does to a row the user has already /// arranged, across an upgrade. void main() { late SettingStore setting; - setUp(() { - SqliteDb.openInMemory(); + setUp(() async { + await openTestDb(); setting = SettingStore.forTest(); getIt.registerSingleton(setting); }); diff --git a/test/server_store_test.dart b/test/server_store_test.dart new file mode 100644 index 0000000000..c7ec777620 --- /dev/null +++ b/test/server_store_test.dart @@ -0,0 +1,217 @@ +import 'dart:convert'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:server_box/data/model/server/custom.dart'; +import 'package:server_box/data/model/server/monitor_http_credential.dart'; +import 'package:server_box/data/model/server/server_private_info.dart'; +import 'package:server_box/data/model/server/ssh_credential.dart'; +import 'package:server_box/data/model/server/system.dart'; +import 'package:server_box/data/model/server/wol_cfg.dart'; +import 'package:server_box/data/store/server.dart'; + +import 'helpers/test_db.dart'; + +/// A [Spi] is six tables now, so the question is whether it survives being +/// taken apart and put back together. +void main() { + late ServerStore store; + + setUp(() async { + await openTestDb(); + store = ServerStore.forTest(); + }); + + tearDown(() async => SqliteDb.close()); + + const rich = Spi( + id: 'srv-1', + name: 'prod', + autoConnect: false, + tags: ['prod', 'db'], + envs: {'TERM': 'xterm', 'LANG': 'C'}, + disabledCmdTypes: ['sensors'], + customSystemType: SystemType.linux, + ssh: SshCredential( + ip: '10.0.0.1', + port: 2222, + user: 'admin', + pwd: 'hunter2', + alterUrl: 'a@b:22', + proxyCommand: 'nc %h %p', + ), + wolCfg: WakeOnLanCfg(mac: 'AA:BB:CC:DD:EE:FF', ip: '10.0.0.255'), + custom: ServerCustom( + pveAddr: 'https://pve:8006', + pveIgnoreCert: true, + cmds: {'up': 'uptime'}, + tempIsCelsius: false, + netDev: 'eth0', + ), + ); + + test('a record survives the round trip through six tables', () { + store.put(rich); + store.invalidate(); + + final got = store.fetchOneRaw('srv-1')!; + expect(got.name, 'prod'); + expect(got.autoConnect, isFalse); + expect(got.tags?.toSet(), {'prod', 'db'}, + reason: 'a tag is membership; the child table keeps no order'); + expect(got.envs, {'TERM': 'xterm', 'LANG': 'C'}); + expect(got.disabledCmdTypes, ['sensors']); + expect(got.customSystemType, SystemType.linux); + expect(got.ssh?.ip, '10.0.0.1'); + expect(got.ssh?.port, 2222); + expect(got.ssh?.pwd, 'hunter2'); + expect(got.ssh?.proxyCommand, 'nc %h %p'); + expect(got.wolCfg?.mac, 'AA:BB:CC:DD:EE:FF'); + expect(got.custom?.pveIgnoreCert, isTrue); + expect(got.custom?.cmds, {'up': 'uptime'}); + expect(got.custom?.tempIsCelsius, isFalse); + }); + + test('a monitor server round trips too', () { + store.put(const Spi( + id: 'mon', + name: 'agent', + monitorHttp: MonitorHttpCredential(addr: 'https://h:3770', pwd: 'p'), + )); + store.invalidate(); + + final got = store.fetchOneRaw('mon')!; + expect(got.ssh, isNull); + expect(got.monitorHttp?.addr, 'https://h:3770'); + expect(got.monitorHttp?.pwd, 'p'); + }); + + test('what an update drops is really dropped', () { + store.put(rich); + store.update(rich, rich.copyWith(tags: ['prod'], envs: null)); + store.invalidate(); + + final got = store.fetchOneRaw('srv-1')!; + expect(got.tags, ['prod'], reason: 'the removed tag is gone'); + expect(got.envs, anyOf(isNull, isEmpty)); + }); + + test('deleting takes the child rows with it', () { + store.put(rich); + store.trustHost('srv-1', 'ssh-ed25519', 'SHA256:x'); + store.deleteById('srv-1'); + + for (final t in const ['server_tag', 'server_env', 'known_host']) { + expect( + SqliteDb.instance.select('SELECT count(*) AS n FROM $t;').single['n'], + 0, + reason: t, + ); + } + // And says so, so a peer does not put it back. + expect( + SqliteDb.instance + .select("SELECT count(*) AS n FROM tombstone WHERE row_id='srv-1';") + .single['n'], + 1, + ); + }); + + test('a write stamps the row for the incremental sync', () { + store.put(rich); + final first = SqliteDb.instance + .select('SELECT updated_at, rev FROM server;') + .single; + expect(first['updated_at'], greaterThan(0)); + + store.put(rich.copyWith(name: 'renamed')); + final second = SqliteDb.instance + .select('SELECT updated_at, rev FROM server;') + .single; + expect(second['rev'], greaterThan(first['rev'] as int), + reason: 'two edits in one millisecond still differ'); + }); + + test('tags are answered by the database', () { + store.put(rich); + store.put(const Spi( + id: 'srv-2', + name: 'other', + tags: ['prod'], + ssh: SshCredential(ip: '10.0.0.2', user: 'root', port: 22), + )); + + expect(store.idsWithTag('prod')..sort(), ['srv-1', 'srv-2']); + expect(store.allTags(), ['db', 'prod']); + }); + + test('a jump host that does not exist is not written', () { + store.put(rich.copyWith( + ssh: rich.ssh!.copyWith(jumpIds: ['nope']), + )); + store.invalidate(); + expect(store.fetchOneRaw('srv-1')!.ssh?.jumpIds, anyOf(isNull, isEmpty)); + }); + + group('restoring a backup', () { + /// What a backup file holds, rather than what `toJson` returns. + /// + /// `Spi.toJson` leaves the nested `SshCredential` as an object — the + /// backup encoder is what walks it — so a merge is only ever handed the + /// decoded form. + Map asStored(Spi spi) => Map.from( + json.decode( + json.encode(spi.toJson(), toEncodable: (o) => (o as dynamic).toJson()), + ) as Map, + ); + + const jumper = Spi( + id: 'srv-a', + name: 'jumper', + ssh: SshCredential( + ip: '10.0.0.10', + user: 'root', + port: 22, + jumpIds: ['srv-b'], + ), + ); + const target = Spi( + id: 'srv-b', + name: 'target', + ssh: SshCredential(ip: '10.0.0.11', user: 'root', port: 22), + ); + + test('a jump host named before it arrives is still linked', () { + // The order a backup lists them in is not the order the rows can be + // written in: a jump host is a server, so `srv-a` names a row that does + // not exist yet. Writing the link inline dropped it silently. + store.replaceAll([jumper, target]); + + expect(store.fetchOneRaw('srv-a')!.ssh?.jumpIds, ['srv-b']); + }); + + test('a backup carrying no timestamps still adds its records', () { + // Every older envelope is like this. Comparing timestamps first made the + // absent one a tie against a record this device does not have, and the + // whole backup restored as nothing. + final added = store.merge({ + 'srv-b': asStored(target), + }, force: false); + + expect(added, isTrue); + expect(store.fetchOneRaw('srv-b')?.name, 'target'); + expect( + store.timestamps['srv-b'], + greaterThan(0), + reason: 'stamped as now, not as older than everything', + ); + }); + + test('a record the backup never knew about is not deleted', () { + store.put(rich); + store.merge({'srv-b': asStored(target)}, force: false); + + expect(store.fetchOneRaw('srv-1'), isNotNull); + }); + }); +} diff --git a/test/settings_menu_test.dart b/test/settings_menu_test.dart index 5cf3e99aa0..b9d8d314a9 100644 --- a/test/settings_menu_test.dart +++ b/test/settings_menu_test.dart @@ -12,6 +12,8 @@ import 'package:server_box/data/store/setting.dart'; import 'package:server_box/generated/l10n/l10n.dart'; import 'package:server_box/view/page/setting/entry.dart'; +import 'helpers/test_db.dart'; + /// One tree, two ways of walking it. /// /// Above 800 it is a menu beside the content, showing every level and opening @@ -25,7 +27,7 @@ void main() { setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-settings-'); - SqliteDb.openInMemory(); + await openTestDb(); // In memory: a real write started in a `testWidgets` body never lets go of // the box's lock, and this page writes on nearly every switch. getIt.registerSingleton(SettingStore.forTest()); diff --git a/test/snippet_list_test.dart b/test/snippet_list_test.dart index b12fcb9d81..d09f6ff4d6 100644 --- a/test/snippet_list_test.dart +++ b/test/snippet_list_test.dart @@ -12,6 +12,8 @@ import 'package:server_box/data/store/snippet.dart'; import 'package:server_box/generated/l10n/l10n.dart'; import 'package:server_box/view/page/snippet/list.dart'; +import 'helpers/test_db.dart'; + /// There has to be a way to make the first snippet, and to search for one, at /// every width. /// @@ -27,7 +29,7 @@ void main() { setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-snippet-'); - SqliteDb.openInMemory(); + await openTestDb(); // In memory: this page persists the pane width on every drag, and a real // write started in a `testWidgets` body never lets go of the box's lock. getIt.registerSingleton(SettingStore.forTest()); @@ -92,7 +94,7 @@ void main() { testWidgets('a wide window with snippets lists them under the same row', ( tester, ) async { - Stores.snippet.put(const Snippet(name: 'deploy', script: 'echo hi')); + Stores.snippet.put(const Snippet(id: 'sn-1', name: 'deploy', script: 'echo hi')); await pump(tester, width: 1200); diff --git a/test/snippet_local_test.dart b/test/snippet_local_test.dart index c1b8732a29..ad33c4da23 100644 --- a/test/snippet_local_test.dart +++ b/test/snippet_local_test.dart @@ -20,7 +20,7 @@ void main() { r'sshpass -p ${pwd} true', ]) { expect( - Snippet(name: 'n', script: script).needsServer, + Snippet(id: 'n', name: 'n', script: script).needsServer, isTrue, reason: script, ); @@ -37,7 +37,7 @@ void main() { r'${ctrl+c}', ]) { expect( - Snippet(name: 'n', script: script).needsServer, + Snippet(id: 'n', name: 'n', script: script).needsServer, isFalse, reason: script, ); @@ -47,7 +47,7 @@ void main() { group('formatting', () { test('a server answers its own placeholders', () { - const snippet = Snippet(name: 'n', script: r'ssh ${user}@${host} -p ${port}'); + const snippet = Snippet(id: 'n', name: 'n', script: r'ssh ${user}@${host} -p ${port}'); expect(snippet.fmtWithSpi(spi), 'ssh lk@10.0.0.1 -p 2222'); }); @@ -56,7 +56,7 @@ void main() { // here to substitute. Substituting an empty string would have turned // `ssh ${user}@${host}` into `ssh @`, which is a different command // rather than a refusal — hence [Snippet.needsServer] filtering first. - const snippet = Snippet(name: 'n', script: 'df -h'); + const snippet = Snippet(id: 'n', name: 'n', script: 'df -h'); expect(snippet.fmtWithSpi(null), 'df -h'); }); }); diff --git a/test/ssh_tab_restore_test.dart b/test/ssh_tab_restore_test.dart index b009fd07c9..2c728aa80d 100644 --- a/test/ssh_tab_restore_test.dart +++ b/test/ssh_tab_restore_test.dart @@ -17,6 +17,7 @@ import 'package:server_box/view/page/ssh/page/page.dart'; import 'package:server_box/view/page/ssh/tab.dart'; import 'helpers/spi_fixture.dart'; +import 'helpers/test_db.dart'; /// What comes back when the app is opened again. /// @@ -31,7 +32,7 @@ void main() { setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-sshtab-'); - SqliteDb.openInMemory(); + await openTestDb(); // In memory: this tree writes as it builds, and a test has no // business leaving a database behind. getIt.registerSingleton(SettingStore.forTest()); diff --git a/test/tables_schema_test.dart b/test/tables_schema_test.dart new file mode 100644 index 0000000000..734deacd9c --- /dev/null +++ b/test/tables_schema_test.dart @@ -0,0 +1,280 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:server_box/data/store/db.dart'; +import 'package:server_box/data/store/tables.dart'; +import 'package:sqlite3/sqlite3.dart'; + +/// What the schema itself guarantees, rather than what the Dart above it +/// remembers to do. +/// +/// Each of these was previously a rule living in one call site: the SSH/monitor +/// exclusivity in `Spix.validate()`, the orphan cleanup in `delServer`, the +/// uniqueness of a snippet name in whatever dialog last checked. A rule in one +/// call site is a rule until someone adds a second call site. +void main() { + late Database db; + + late AppDb appDb; + + // Created by Drift, asserted through the raw handle it was handed. Drift + // owns the DDL; these are the guarantees it has to deliver. + setUp(() async { + db = sqlite3.openInMemory(); + db.execute('PRAGMA foreign_keys = ON;'); + appDb = AppDb(NativeDatabase.opened(db)); + // Forces `onCreate`; nothing exists until the executor opens. + await appDb.customStatement('SELECT 1;'); + }); + + tearDown(() async { + await appDb.close(); + }); + + void addServer( + String id, { + String? sshIp = '10.0.0.1', + String? monitorAddr, + String? keyId, + }) { + db.execute( + 'INSERT INTO server (id, name, ssh_ip, ssh_port, ssh_user, ssh_key_id, ' + 'monitor_addr) VALUES (?, ?, ?, ?, ?, ?, ?);', + [ + id, + 'srv $id', + sshIp, + sshIp == null ? null : 22, + sshIp == null ? null : 'root', + keyId, + monitorAddr, + ], + ); + } + + test('Drift creates exactly the tables the app names', () { + final tables = db + .select("SELECT name FROM sqlite_master WHERE type='table';") + .map((r) => r['name'] as String) + .where((n) => !n.startsWith('sqlite_')); + expect(tables.toSet(), Tables.names.toSet()); + }); + + group('a server is reached one way or the other', () { + test('SSH alone is accepted', () { + expect(() => addServer('a'), returnsNormally); + }); + + test('a monitor agent alone is accepted', () { + expect( + () => addServer('b', sshIp: null, monitorAddr: 'https://h:3770'), + returnsNormally, + ); + }); + + test('both at once is refused', () { + expect( + () => addServer('c', monitorAddr: 'https://h:3770'), + throwsA(isA()), + ); + }); + + test('neither is refused', () { + expect( + () => addServer('d', sshIp: null), + throwsA(isA()), + ); + }); + }); + + test('deleting a server takes everything hanging off it', () { + addServer('srv'); + db.execute("INSERT INTO server_tag VALUES ('srv', 'prod');"); + db.execute("INSERT INTO server_env VALUES ('srv', 'TERM', 'xterm');"); + db.execute("INSERT INTO server_disabled_cmd VALUES ('srv', 'sensors');"); + db.execute("INSERT INTO server_custom_cmd VALUES ('srv', 'up', 'uptime');"); + db.execute("INSERT INTO known_host VALUES ('srv', 'ssh-ed25519', 'SHA256:x');"); + db.execute( + 'INSERT INTO port_forward (id, server_id, name, type, local_port) ' + "VALUES ('pf', 'srv', 'pg', 'local', 15432);", + ); + db.execute( + "INSERT INTO conn_stat VALUES ('cs', 'srv', 'srv', 1, 'success', '', 5);", + ); + + db.execute("DELETE FROM server WHERE id = 'srv';"); + + for (final t in const [ + 'server_tag', + 'server_env', + 'server_disabled_cmd', + 'server_custom_cmd', + 'known_host', + 'port_forward', + 'conn_stat', + ]) { + expect( + db.select('SELECT count(*) AS n FROM $t;').single['n'], + 0, + reason: '$t still has rows for a server that is gone', + ); + } + }); + + test('deleting a private key keeps the servers that used it', () { + db.execute('INSERT INTO private_key (id, name, key) ' + "VALUES ('k1', 'work', 'PRIVATE');"); + addServer('srv', keyId: 'k1'); + + db.execute("DELETE FROM private_key WHERE id = 'k1';"); + + final row = db.select("SELECT ssh_key_id FROM server WHERE id = 'srv';"); + expect(row.length, 1, reason: 'the server must survive'); + expect(row.single['ssh_key_id'], isNull, reason: 'and ask for a new key'); + }); + + test('a jump host that is deleted stops being one', () { + addServer('a'); + addServer('b'); + db.execute("INSERT INTO server_jump VALUES ('a', 0, 'b');"); + + db.execute("DELETE FROM server WHERE id = 'b';"); + + expect(db.select('SELECT count(*) AS n FROM server_jump;').single['n'], 0); + expect(db.select("SELECT count(*) AS n FROM server WHERE id = 'a';") + .single['n'], 1); + }); + + test('an auto-run target that is deleted stops being one', () { + addServer('srv'); + db.execute('INSERT INTO snippet (id, name, script) ' + "VALUES ('s1', 'deploy', 'echo');"); + db.execute("INSERT INTO snippet_auto_run_on VALUES ('s1', 'srv');"); + + db.execute("DELETE FROM server WHERE id = 'srv';"); + + expect( + db.select('SELECT count(*) AS n FROM snippet_auto_run_on;').single['n'], + 0, + ); + expect(db.select('SELECT count(*) AS n FROM snippet;').single['n'], 1, + reason: 'the snippet itself is not a per-server thing'); + }); + + test('a name the user typed is not a key, but is still unique', () { + db.execute('INSERT INTO snippet (id, name, script) ' + "VALUES ('s1', 'deploy', 'echo a');"); + // A different snippet, same name. + expect( + () => db.execute('INSERT INTO snippet (id, name, script) ' + "VALUES ('s2', 'deploy', 'b');"), + throwsA(isA()), + ); + // Renaming is one column, and s1 keeps its identity. + db.execute("UPDATE snippet SET name = 'release' WHERE id = 's1';"); + expect(db.select("SELECT name FROM snippet WHERE id = 's1';") + .single['name'], 'release'); + }); + + test('a port forward names a real server and a real type', () { + expect( + () => db.execute( + 'INSERT INTO port_forward (id, server_id, name, type, local_port) ' + "VALUES ('pf', 'nope', 'x', 'local', 1);", + ), + throwsA(isA()), + reason: 'no such server', + ); + + addServer('srv'); + expect( + () => db.execute( + 'INSERT INTO port_forward (id, server_id, name, type, local_port) ' + "VALUES ('pf', 'srv', 'x', 'sideways', 1);", + ), + throwsA(isA()), + reason: 'no such forward type', + ); + }); + + test('a port number has to be one', () { + expect( + () => db.execute( + 'INSERT INTO server (id, name, ssh_ip, ssh_port, ssh_user) ' + "VALUES ('x', 'x', '10.0.0.1', 70000, 'root');", + ), + throwsA(isA()), + ); + }); + + group('sync metadata', () { + test('every sync root carries updated_at and rev', () { + for (final t in Tables.syncRoots) { + final cols = db + .select('PRAGMA table_info($t);') + .map((r) => r['name'] as String) + .toSet(); + expect(cols, containsAll(['updated_at', 'rev']), reason: t); + } + }); + + test('a child table has neither: it moves with its parent', () { + for (final t in const ['server_tag', 'server_env', 'server_jump']) { + final cols = db + .select('PRAGMA table_info($t);') + .map((r) => r['name'] as String) + .toSet(); + expect(cols, isNot(contains('updated_at')), reason: t); + } + }); + + test('an incremental pull is one indexed query per root', () { + addServer('a'); + db.execute("UPDATE server SET updated_at = 100 WHERE id = 'a';"); + addServer('b'); + db.execute("UPDATE server SET updated_at = 300 WHERE id = 'b';"); + + final since = db + .select('SELECT id FROM server WHERE updated_at > ? ORDER BY id;', [200]) + .map((r) => r['id']) + .toList(); + expect(since, ['b'], reason: 'only what changed since the watermark'); + }); + + test('a deletion is a fact that survives, so it can travel', () { + addServer('gone'); + db.execute("DELETE FROM server WHERE id = 'gone';"); + // The store records it; the schema is what makes the record possible. + db.execute("INSERT INTO tombstone VALUES ('server', 'gone', 500);"); + + final deleted = db + .select('SELECT row_id FROM tombstone WHERE tbl = ? AND deleted_at > ?;', + ['server', 400]) + .map((r) => r['row_id']) + .toList(); + expect(deleted, ['gone'], + reason: 'without this a peer re-adds the row it still has'); + }); + }); + + test('tags are queryable, not decodable', () { + addServer('a'); + addServer('b'); + db.execute("INSERT INTO server_tag VALUES ('a', 'prod'), ('b', 'prod'), " + "('a', 'db');"); + + // The question the server list asks, as one statement rather than a decode + // of every record. + final tagged = db + .select("SELECT server_id FROM server_tag WHERE tag = 'prod' " + 'ORDER BY server_id;') + .map((r) => r['server_id']) + .toList(); + expect(tagged, ['a', 'b']); + + final allTags = db + .select('SELECT DISTINCT tag FROM server_tag ORDER BY tag;') + .map((r) => r['tag']) + .toList(); + expect(allTags, ['db', 'prod']); + }); +} diff --git a/test/terminal_clipboard_test.dart b/test/terminal_clipboard_test.dart index ef64a8b0f9..ad2686d126 100644 --- a/test/terminal_clipboard_test.dart +++ b/test/terminal_clipboard_test.dart @@ -16,6 +16,7 @@ import 'package:xterm/core.dart'; import 'package:xterm/ui.dart'; import 'helpers/fake_shell.dart'; +import 'helpers/test_db.dart'; /// Right-click in the terminal, which is copy or paste depending on nothing /// but whether anything is selected. @@ -32,7 +33,7 @@ void main() { setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-term-'); - SqliteDb.openInMemory(); + await openTestDb(); getIt.registerSingleton(SettingStore.forTest()); clipboard.clear();