From 91d71026b4d6f69409c7ebbbcc4f7fec5391a150 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 19 Apr 2023 17:11:23 -0700 Subject: [PATCH 01/11] login: Use canonical realm URL from server Just the other day we were talking about this get-server-settings property, and I explained why it's important for a mobile client to use it: https://chat.zulip.org/#narrow/stream/378-api-design/topic/uri.20to.20url.20compatibility/near/1549242 But apparently when I wrote this code a couple of weeks earlier, I'd forgotten about that, oops. Fix it. It's particularly useful that we fix this before we start keeping the list of accounts in a database, so that there's no legacy of some people's devices having a non-canonicalized URL lying around. --- lib/widgets/login.dart | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/widgets/login.dart b/lib/widgets/login.dart index 5deddd6a02..f85c4ed2b1 100644 --- a/lib/widgets/login.dart +++ b/lib/widgets/login.dart @@ -55,7 +55,7 @@ class _AddAccountPageState extends State { // TODO(#36): support login methods beyond email/password Navigator.push(context, - EmailPasswordLoginPage.buildRoute(realmUrl: url, serverSettings: serverSettings)); + EmailPasswordLoginPage.buildRoute(serverSettings: serverSettings)); } @override @@ -82,16 +82,13 @@ class _AddAccountPageState extends State { } class EmailPasswordLoginPage extends StatefulWidget { - const EmailPasswordLoginPage({ - super.key, required this.realmUrl, required this.serverSettings}); + const EmailPasswordLoginPage({super.key, required this.serverSettings}); - final Uri realmUrl; final GetServerSettingsResult serverSettings; - static Route buildRoute({ - required Uri realmUrl, required GetServerSettingsResult serverSettings}) { + static Route buildRoute({required GetServerSettingsResult serverSettings}) { return _LoginSequenceRoute(builder: (context) => - EmailPasswordLoginPage(realmUrl: realmUrl, serverSettings: serverSettings)); + EmailPasswordLoginPage(serverSettings: serverSettings)); } @override @@ -105,14 +102,14 @@ class _EmailPasswordLoginPageState extends State { Future _getUserId(FetchApiKeyResult fetchApiKeyResult) async { final FetchApiKeyResult(:email, :apiKey) = fetchApiKeyResult; final auth = Auth( - realmUrl: widget.realmUrl, email: email, apiKey: apiKey); + realmUrl: widget.serverSettings.realmUri, email: email, apiKey: apiKey); final connection = LiveApiConnection(auth: auth); // TODO make this widget testable return (await getOwnUser(connection)).userId; } void _submit() async { final context = _emailKey.currentContext!; - final realmUrl = widget.realmUrl; + final realmUrl = widget.serverSettings.realmUri; final String? email = _emailKey.currentState!.value; final String? password = _passwordKey.currentState!.value; if (email == null || password == null) { From 8f94abc19734542dd9a4f5168e8cdd20d33886db Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 17 Apr 2023 11:54:51 -0700 Subject: [PATCH 02/11] api [nfc]: Let authHeader take email and apiKey separately This removes the one place where we really use the fact that we've had Account as a subtype of Auth. We'll be giving up that subtyping as part of getting Account from a database ORM. --- lib/api/core.dart | 10 ++++++---- lib/widgets/content.dart | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/api/core.dart b/lib/api/core.dart index fbbdb705ac..aa14f2d114 100644 --- a/lib/api/core.dart +++ b/lib/api/core.dart @@ -42,9 +42,9 @@ abstract class ApiConnection { Future postFileFromStream(String route, Stream> content, int length, { String? filename }); } -// TODO memoize -Map authHeader(Auth auth) { - final authBytes = utf8.encode("${auth.email}:${auth.apiKey}"); +// TODO memoize auth header on LiveApiConnection and PerAccountStore +Map authHeader({required String email, required String apiKey}) { + final authBytes = utf8.encode("$email:$apiKey"); return { 'Authorization': 'Basic ${base64.encode(authBytes)}', }; @@ -65,7 +65,9 @@ class LiveApiConnection extends ApiConnection { _isOpen = false; } - Map _headers() => authHeader(auth); + Map _headers() { + return authHeader(email: auth.email, apiKey: auth.apiKey); + } @override Future get(String route, Map? params) async { diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index 0308a5726e..c41fafa5b6 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -563,7 +563,7 @@ class RealmContentNetworkImage extends StatelessWidget { @override Widget build(BuildContext context) { - final Auth auth = PerAccountStoreWidget.of(context).account; + final account = PerAccountStoreWidget.of(context).account; final Uri parsedSrc = Uri.parse(src); @@ -591,8 +591,8 @@ class RealmContentNetworkImage extends StatelessWidget { isAntiAlias: isAntiAlias, // Only send the auth header to the server `auth` belongs to. - headers: parsedSrc.origin == auth.realmUrl.origin - ? authHeader(auth) + headers: parsedSrc.origin == account.realmUrl.origin + ? authHeader(email: account.email, apiKey: account.apiKey) : null, cacheWidth: cacheWidth, From 8eea74db2a656687d026b611ad066f74c78b69c1 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 17 Apr 2023 11:56:22 -0700 Subject: [PATCH 03/11] store [nfc]: Make Account not extend Auth This frees us up to switch to an Account type that's generated to correspond to a database table. --- lib/model/store.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/model/store.dart b/lib/model/store.dart index 394c2da670..dba563798a 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -203,18 +203,21 @@ class PerAccountStore extends ChangeNotifier { } @immutable -class Account extends Auth { - Account({ - required super.realmUrl, - required super.email, - required super.apiKey, +class Account { + const Account({ + required this.realmUrl, required this.userId, + required this.email, + required this.apiKey, required this.zulipFeatureLevel, required this.zulipVersion, required this.zulipMergeBase, }); + final Uri realmUrl; final int userId; + final String email; + final String apiKey; final int zulipFeatureLevel; final String zulipVersion; final String? zulipMergeBase; @@ -271,7 +274,8 @@ class LivePerAccountStore extends PerAccountStore { /// /// In the future this might load an old snapshot from local storage first. static Future load(Account account) async { - final connection = LiveApiConnection(auth: account); + final connection = LiveApiConnection( + auth: Auth(realmUrl: account.realmUrl, email: account.email, apiKey: account.apiKey)); final stopwatch = Stopwatch()..start(); final initialSnapshot = await registerQueue(connection); // TODO retry From b3d1af5f486c21aa3dd0858d52158dfe602c1fcb Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 17 Apr 2023 12:15:05 -0700 Subject: [PATCH 04/11] store [nfc]: Separate insertAccount from per-impl doInsertAccount This will let LiveGlobalStore and TestGlobalStore diverge in how the data is ultimately stored and how the account IDs are assigned, while sharing the code to maintain the in-Dart cache. --- lib/model/store.dart | 18 ++++++++++++++---- test/model/store_test.dart | 9 +++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/model/store.dart b/lib/model/store.dart index dba563798a..a66b4cb0d7 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -31,6 +31,7 @@ abstract class GlobalStore extends ChangeNotifier { GlobalStore({required Map accounts}) : _accounts = accounts; + /// A cache of the [Accounts] table in the underlying data store. final Map _accounts; // TODO settings (those that are per-device rather than per-account) @@ -107,18 +108,18 @@ abstract class GlobalStore extends ChangeNotifier { // TODO(#13): rewrite these setters/mutators with a database - int _nextAccountId = 1; - /// Add an account to the store, returning its assigned account ID. Future insertAccount(Account account) async { - final accountId = _nextAccountId; - _nextAccountId++; + final accountId = await doInsertAccount(account); assert(!_accounts.containsKey(accountId)); _accounts[accountId] = account; notifyListeners(); return accountId; } + /// Add an account to the underlying data store. + Future doInsertAccount(Account account); + // More mutators as needed: // Future updateAccount... } @@ -240,6 +241,15 @@ class LiveGlobalStore extends GlobalStore { Future loadPerAccount(Account account) { return LivePerAccountStore.load(account); } + + int _nextAccountId = 1; + + @override + Future doInsertAccount(Account account) async { + final accountId = _nextAccountId; + _nextAccountId++; + return accountId; + } } /// A scaffolding hack for while prototyping. diff --git a/test/model/store_test.dart b/test/model/store_test.dart index dd716ae583..cd09ee0fdd 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -109,4 +109,13 @@ class TestGlobalStore extends GlobalStore { (completers[account] ??= []).add(completer); return completer.future; } + + int _nextAccountId = 1; + + @override + Future doInsertAccount(Account account) async { + final accountId = _nextAccountId; + _nextAccountId++; + return accountId; + } } From cf62cc6aec3785f1bc8f4a46fb54683ca0088a19 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 10 Apr 2023 11:55:12 -0700 Subject: [PATCH 05/11] deps: Upgrade with `flutter pub upgrade` --- pubspec.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index b5fe0a93c7..6fe21c9335 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -126,7 +126,7 @@ packages: description: path: "pkgs/checks" ref: HEAD - resolved-ref: "78329319a535e3b7cc7bd056c149b47ff4149c57" + resolved-ref: "7fab0792d346025c2782b758819bf3449e3b5a8b" url: "git@github.com:dart-lang/test.git" source: git version: "0.2.2-dev" @@ -214,10 +214,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + sha256: "435383ca05f212760b0a70426b5a90354fe6bd65992b3a5e27ab6ede74c02f5c" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" device_info_plus_platform_interface: dependency: transitive description: @@ -254,10 +254,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "0d923fb610d0abf67f2149c3a50ef85f78bebecfc4d645719ca70bcf4abc788f" + sha256: b85eb92b175767fdaa0c543bf3b0d1f610fe966412ea72845fe5ba7801e763ff url: "https://pub.dev" source: hosted - version: "5.2.7" + version: "5.2.10" fixnum: dependency: transitive description: @@ -357,10 +357,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" io: dependency: transitive description: @@ -477,18 +477,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" + sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.25" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "12eee51abdf4d34c590f043f45073adbb45514a108bd9db4491547a2fd891059" + sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" path_provider_linux: dependency: transitive description: @@ -565,18 +565,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625" + sha256: "692261968a494e47323dcc8bc66d8d52e81bc27cb4b808e4e8d7e8079d4cc01a" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "82ddd4ab9260c295e6e39612d4ff00390b9a7a21f1bb1da771e2f232d80ab8a1" + sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" shelf: dependency: transitive description: @@ -802,10 +802,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" webkit_inspection_protocol: dependency: transitive description: @@ -818,10 +818,10 @@ packages: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" xdg_directories: dependency: transitive description: From 483a3ef1df4ac2286bc52597ed06e726d69547b8 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 27 Feb 2023 14:19:09 -0800 Subject: [PATCH 06/11] deps: Add drift, sqlite3_flutter_libs, path_provider, path, dev:drift_dev These are the dependencies called for by the Drift docs: https://drift.simonbinder.eu/docs/getting-started/#adding-the-dependency --- ios/Podfile.lock | 21 +++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 24 ++++++ pubspec.lock | 76 ++++++++++++++++++- pubspec.yaml | 5 ++ .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 9 files changed, 135 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d1ddbbbd44..1f3f75d8b8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -44,6 +44,21 @@ PODS: - SDWebImage/Core (5.15.5) - share_plus (0.0.1): - Flutter + - sqlite3 (3.41.0): + - sqlite3/common (= 3.41.0) + - sqlite3/common (3.41.0) + - sqlite3/fts5 (3.41.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.41.0): + - sqlite3/common + - sqlite3/rtree (3.41.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - sqlite3 (~> 3.41.0) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree - SwiftyGif (5.4.4) DEPENDENCIES: @@ -52,12 +67,14 @@ DEPENDENCIES: - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery - SDWebImage + - sqlite3 - SwiftyGif EXTERNAL SOURCES: @@ -71,6 +88,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" share_plus: :path: ".symlinks/plugins/share_plus/ios" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" SPEC CHECKSUMS: device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed @@ -81,6 +100,8 @@ SPEC CHECKSUMS: path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 + sqlite3: d31b2b69d59bd1b4ab30e5c92eb18fd8e82fa392 + sqlite3_flutter_libs: 78f93cb854d4680595bc2c63c57209a104b2efb1 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f PODFILE CHECKSUM: 985e5b058f26709dc81f9ae74ea2b2775bdbcefe diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f6f23bfe97..4c0025f9e9 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f16b4c3421..ad279a8f42 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + sqlite3_flutter_libs url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 20cf383ed8..541a0cfaa4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,9 +8,11 @@ import Foundation import device_info_plus import path_provider_foundation import share_plus +import sqlite3_flutter_libs func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 203bc88d11..84d7c886bc 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -7,12 +7,32 @@ PODS: - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS + - sqlite3 (3.41.0): + - sqlite3/common (= 3.41.0) + - sqlite3/common (3.41.0) + - sqlite3/fts5 (3.41.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.41.0): + - sqlite3/common + - sqlite3/rtree (3.41.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - FlutterMacOS + - sqlite3 (~> 3.41.0) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree DEPENDENCIES: - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + +SPEC REPOS: + trunk: + - sqlite3 EXTERNAL SOURCES: device_info_plus: @@ -23,12 +43,16 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos SPEC CHECKSUMS: device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + sqlite3: d31b2b69d59bd1b4ab30e5c92eb18fd8e82fa392 + sqlite3_flutter_libs: f20746e4a0245afbee4f20d9afc0072ebff7cc26 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 diff --git a/pubspec.lock b/pubspec.lock index 6fe21c9335..a291389aa5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.10.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" args: dependency: transitive description: @@ -113,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -130,6 +146,14 @@ packages: url: "git@github.com:dart-lang/test.git" source: git version: "0.2.2-dev" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" clock: dependency: transitive description: @@ -226,6 +250,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + drift: + dependency: "direct main" + description: + name: drift + sha256: "1eaef0a152f1b3dc2e3ad3b04f900794bbe5a2833c26a85794ed1f7e5b7320ce" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: b6c2b1bcf637d34142bf9a0c21d1d290ade2254538be00559360db165b524381 + url: "https://pub.dev" + source: hosted + version: "2.7.0" fake_async: dependency: transitive description: @@ -458,7 +498,7 @@ packages: source: hosted version: "2.1.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" @@ -466,7 +506,7 @@ packages: source: hosted version: "1.8.3" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 @@ -561,6 +601,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" share_plus: dependency: "direct main" description: @@ -654,6 +702,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: a3ba4b66a7ab170ce7aa3f5ac43c19ee8d6637afbe7b7c95c94112b4f4d91566 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: "02f80aea54a19a36b347dedf6d4181ecd9107f5831ea6139cfd0376a3de197ba" + url: "https://pub.dev" + source: hosted + version: "0.5.13" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "11ebfd764085a96261f44f90cbf475927c508e654d4be30ee4832d564d06d86b" + url: "https://pub.dev" + source: hosted + version: "0.28.1" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0deef45732..8b24617df4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,10 @@ dependencies: share_plus: ^6.3.1 device_info_plus: ^8.1.0 file_picker: ^5.2.7 + drift: ^2.5.0 + path_provider: ^2.0.13 + path: ^1.8.3 + sqlite3_flutter_libs: ^0.5.13 dev_dependencies: flutter_test: @@ -70,6 +74,7 @@ dev_dependencies: git: url: git@github.com:dart-lang/test.git path: pkgs/checks + drift_dev: ^2.5.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c3384ec523..0143d6e063 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 01d383628b..b70772609d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST share_plus + sqlite3_flutter_libs url_launcher_windows ) From 780b09283d144c1dd48185d138e019ef10de1c96 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 27 Feb 2023 14:31:16 -0800 Subject: [PATCH 07/11] db: Write a first Drift schema Also add a .gitattributes file, excluding `*.g.dart` files from diffs. We didn't really need this when the only such files were the JSON serialization/deserialization files in the API bindings, because those are pretty compact; but this generated ORM code is much longer and makes it more necessary. --- .gitattributes | 9 + lib/model/database.dart | 58 +++++ lib/model/database.g.dart | 431 ++++++++++++++++++++++++++++++++++ test/model/database_test.dart | 55 +++++ 4 files changed, 553 insertions(+) create mode 100644 .gitattributes create mode 100644 lib/model/database.dart create mode 100644 lib/model/database.g.dart create mode 100644 test/model/database_test.dart diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..64c84c5115 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# Suppress noisy generated files in diffs. + +# Dart files generated from the files next to them: +*.g.dart -diff + +# On the other hand, keep diffs for pubspec.lock. It contains +# information independent of any non-generated file in the tree. +# And thankfully it's much less verbose than, say, a yarn.lock. +#pubspec.lock -diff diff --git a/lib/model/database.dart b/lib/model/database.dart new file mode 100644 index 0000000000..5843325d47 --- /dev/null +++ b/lib/model/database.dart @@ -0,0 +1,58 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +part 'database.g.dart'; + +// TODO unify with Account in store.dart +class Accounts extends Table { + Column get id => integer().autoIncrement()(); + + Column get realmUrl => text().map(const UriConverter())(); + Column get userId => integer()(); + + Column get email => text()(); + Column get apiKey => text()(); + + Column get zulipVersion => text()(); + Column get zulipMergeBase => text().nullable()(); + Column get zulipFeatureLevel => integer()(); + + @override + List>> get uniqueKeys => [ + {realmUrl, userId}, + {realmUrl, email}, + ]; +} + +class UriConverter extends TypeConverter { + const UriConverter(); + @override String toSql(Uri value) => value.toString(); + @override Uri fromSql(String fromDb) => Uri.parse(fromDb); +} + +LazyDatabase _openConnection() { + return LazyDatabase(() async { + // TODO decide if this path is the right one to use + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(path.join(dbFolder.path, 'db.sqlite')); + return NativeDatabase.createInBackground(file); + }); +} + +@DriftDatabase(tables: [Accounts]) +class AppDatabase extends _$AppDatabase { + AppDatabase(QueryExecutor e) : super(e); + + AppDatabase.live() : this(_openConnection()); + + @override + int get schemaVersion => 1; // TODO migrations + + Future createAccount(AccountsCompanion values) { + return into(accounts).insert(values); + } +} diff --git a/lib/model/database.g.dart b/lib/model/database.g.dart new file mode 100644 index 0000000000..ea54de5356 --- /dev/null +++ b/lib/model/database.g.dart @@ -0,0 +1,431 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $AccountsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _realmUrlMeta = + const VerificationMeta('realmUrl'); + @override + late final GeneratedColumnWithTypeConverter realmUrl = + GeneratedColumn('realm_url', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($AccountsTable.$converterrealmUrl); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _emailMeta = const VerificationMeta('email'); + @override + late final GeneratedColumn email = GeneratedColumn( + 'email', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _apiKeyMeta = const VerificationMeta('apiKey'); + @override + late final GeneratedColumn apiKey = GeneratedColumn( + 'api_key', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _zulipVersionMeta = + const VerificationMeta('zulipVersion'); + @override + late final GeneratedColumn zulipVersion = GeneratedColumn( + 'zulip_version', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _zulipMergeBaseMeta = + const VerificationMeta('zulipMergeBase'); + @override + late final GeneratedColumn zulipMergeBase = GeneratedColumn( + 'zulip_merge_base', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _zulipFeatureLevelMeta = + const VerificationMeta('zulipFeatureLevel'); + @override + late final GeneratedColumn zulipFeatureLevel = GeneratedColumn( + 'zulip_feature_level', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [ + id, + realmUrl, + userId, + email, + apiKey, + zulipVersion, + zulipMergeBase, + zulipFeatureLevel + ]; + @override + String get aliasedName => _alias ?? 'accounts'; + @override + String get actualTableName => 'accounts'; + @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)); + } + context.handle(_realmUrlMeta, const VerificationResult.success()); + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('email')) { + context.handle( + _emailMeta, email.isAcceptableOrUnknown(data['email']!, _emailMeta)); + } else if (isInserting) { + context.missing(_emailMeta); + } + if (data.containsKey('api_key')) { + context.handle(_apiKeyMeta, + apiKey.isAcceptableOrUnknown(data['api_key']!, _apiKeyMeta)); + } else if (isInserting) { + context.missing(_apiKeyMeta); + } + if (data.containsKey('zulip_version')) { + context.handle( + _zulipVersionMeta, + zulipVersion.isAcceptableOrUnknown( + data['zulip_version']!, _zulipVersionMeta)); + } else if (isInserting) { + context.missing(_zulipVersionMeta); + } + if (data.containsKey('zulip_merge_base')) { + context.handle( + _zulipMergeBaseMeta, + zulipMergeBase.isAcceptableOrUnknown( + data['zulip_merge_base']!, _zulipMergeBaseMeta)); + } + if (data.containsKey('zulip_feature_level')) { + context.handle( + _zulipFeatureLevelMeta, + zulipFeatureLevel.isAcceptableOrUnknown( + data['zulip_feature_level']!, _zulipFeatureLevelMeta)); + } else if (isInserting) { + context.missing(_zulipFeatureLevelMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + List> get uniqueKeys => [ + {realmUrl, userId}, + {realmUrl, email}, + ]; + @override + Account map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Account( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + realmUrl: $AccountsTable.$converterrealmUrl.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}realm_url'])!), + userId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}user_id'])!, + email: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}email'])!, + apiKey: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}api_key'])!, + zulipVersion: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}zulip_version'])!, + zulipMergeBase: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}zulip_merge_base']), + zulipFeatureLevel: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}zulip_feature_level'])!, + ); + } + + @override + $AccountsTable createAlias(String alias) { + return $AccountsTable(attachedDatabase, alias); + } + + static TypeConverter $converterrealmUrl = const UriConverter(); +} + +class Account extends DataClass implements Insertable { + final int id; + final Uri realmUrl; + final int userId; + final String email; + final String apiKey; + final String zulipVersion; + final String? zulipMergeBase; + final int zulipFeatureLevel; + const Account( + {required this.id, + required this.realmUrl, + required this.userId, + required this.email, + required this.apiKey, + required this.zulipVersion, + this.zulipMergeBase, + required this.zulipFeatureLevel}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + { + final converter = $AccountsTable.$converterrealmUrl; + map['realm_url'] = Variable(converter.toSql(realmUrl)); + } + map['user_id'] = Variable(userId); + map['email'] = Variable(email); + map['api_key'] = Variable(apiKey); + map['zulip_version'] = Variable(zulipVersion); + if (!nullToAbsent || zulipMergeBase != null) { + map['zulip_merge_base'] = Variable(zulipMergeBase); + } + map['zulip_feature_level'] = Variable(zulipFeatureLevel); + return map; + } + + AccountsCompanion toCompanion(bool nullToAbsent) { + return AccountsCompanion( + id: Value(id), + realmUrl: Value(realmUrl), + userId: Value(userId), + email: Value(email), + apiKey: Value(apiKey), + zulipVersion: Value(zulipVersion), + zulipMergeBase: zulipMergeBase == null && nullToAbsent + ? const Value.absent() + : Value(zulipMergeBase), + zulipFeatureLevel: Value(zulipFeatureLevel), + ); + } + + factory Account.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Account( + id: serializer.fromJson(json['id']), + realmUrl: serializer.fromJson(json['realmUrl']), + userId: serializer.fromJson(json['userId']), + email: serializer.fromJson(json['email']), + apiKey: serializer.fromJson(json['apiKey']), + zulipVersion: serializer.fromJson(json['zulipVersion']), + zulipMergeBase: serializer.fromJson(json['zulipMergeBase']), + zulipFeatureLevel: serializer.fromJson(json['zulipFeatureLevel']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'realmUrl': serializer.toJson(realmUrl), + 'userId': serializer.toJson(userId), + 'email': serializer.toJson(email), + 'apiKey': serializer.toJson(apiKey), + 'zulipVersion': serializer.toJson(zulipVersion), + 'zulipMergeBase': serializer.toJson(zulipMergeBase), + 'zulipFeatureLevel': serializer.toJson(zulipFeatureLevel), + }; + } + + Account copyWith( + {int? id, + Uri? realmUrl, + int? userId, + String? email, + String? apiKey, + String? zulipVersion, + Value zulipMergeBase = const Value.absent(), + int? zulipFeatureLevel}) => + Account( + id: id ?? this.id, + realmUrl: realmUrl ?? this.realmUrl, + userId: userId ?? this.userId, + email: email ?? this.email, + apiKey: apiKey ?? this.apiKey, + zulipVersion: zulipVersion ?? this.zulipVersion, + zulipMergeBase: + zulipMergeBase.present ? zulipMergeBase.value : this.zulipMergeBase, + zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ); + @override + String toString() { + return (StringBuffer('Account(') + ..write('id: $id, ') + ..write('realmUrl: $realmUrl, ') + ..write('userId: $userId, ') + ..write('email: $email, ') + ..write('apiKey: $apiKey, ') + ..write('zulipVersion: $zulipVersion, ') + ..write('zulipMergeBase: $zulipMergeBase, ') + ..write('zulipFeatureLevel: $zulipFeatureLevel') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, realmUrl, userId, email, apiKey, + zulipVersion, zulipMergeBase, zulipFeatureLevel); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Account && + other.id == this.id && + other.realmUrl == this.realmUrl && + other.userId == this.userId && + other.email == this.email && + other.apiKey == this.apiKey && + other.zulipVersion == this.zulipVersion && + other.zulipMergeBase == this.zulipMergeBase && + other.zulipFeatureLevel == this.zulipFeatureLevel); +} + +class AccountsCompanion extends UpdateCompanion { + final Value id; + final Value realmUrl; + final Value userId; + final Value email; + final Value apiKey; + final Value zulipVersion; + final Value zulipMergeBase; + final Value zulipFeatureLevel; + const AccountsCompanion({ + this.id = const Value.absent(), + this.realmUrl = const Value.absent(), + this.userId = const Value.absent(), + this.email = const Value.absent(), + this.apiKey = const Value.absent(), + this.zulipVersion = const Value.absent(), + this.zulipMergeBase = const Value.absent(), + this.zulipFeatureLevel = const Value.absent(), + }); + AccountsCompanion.insert({ + this.id = const Value.absent(), + required Uri realmUrl, + required int userId, + required String email, + required String apiKey, + required String zulipVersion, + this.zulipMergeBase = const Value.absent(), + required int zulipFeatureLevel, + }) : realmUrl = Value(realmUrl), + userId = Value(userId), + email = Value(email), + apiKey = Value(apiKey), + zulipVersion = Value(zulipVersion), + zulipFeatureLevel = Value(zulipFeatureLevel); + static Insertable custom({ + Expression? id, + Expression? realmUrl, + Expression? userId, + Expression? email, + Expression? apiKey, + Expression? zulipVersion, + Expression? zulipMergeBase, + Expression? zulipFeatureLevel, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (realmUrl != null) 'realm_url': realmUrl, + if (userId != null) 'user_id': userId, + if (email != null) 'email': email, + if (apiKey != null) 'api_key': apiKey, + if (zulipVersion != null) 'zulip_version': zulipVersion, + if (zulipMergeBase != null) 'zulip_merge_base': zulipMergeBase, + if (zulipFeatureLevel != null) 'zulip_feature_level': zulipFeatureLevel, + }); + } + + AccountsCompanion copyWith( + {Value? id, + Value? realmUrl, + Value? userId, + Value? email, + Value? apiKey, + Value? zulipVersion, + Value? zulipMergeBase, + Value? zulipFeatureLevel}) { + return AccountsCompanion( + id: id ?? this.id, + realmUrl: realmUrl ?? this.realmUrl, + userId: userId ?? this.userId, + email: email ?? this.email, + apiKey: apiKey ?? this.apiKey, + zulipVersion: zulipVersion ?? this.zulipVersion, + zulipMergeBase: zulipMergeBase ?? this.zulipMergeBase, + zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (realmUrl.present) { + final converter = $AccountsTable.$converterrealmUrl; + map['realm_url'] = Variable(converter.toSql(realmUrl.value)); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (apiKey.present) { + map['api_key'] = Variable(apiKey.value); + } + if (zulipVersion.present) { + map['zulip_version'] = Variable(zulipVersion.value); + } + if (zulipMergeBase.present) { + map['zulip_merge_base'] = Variable(zulipMergeBase.value); + } + if (zulipFeatureLevel.present) { + map['zulip_feature_level'] = Variable(zulipFeatureLevel.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AccountsCompanion(') + ..write('id: $id, ') + ..write('realmUrl: $realmUrl, ') + ..write('userId: $userId, ') + ..write('email: $email, ') + ..write('apiKey: $apiKey, ') + ..write('zulipVersion: $zulipVersion, ') + ..write('zulipMergeBase: $zulipMergeBase, ') + ..write('zulipFeatureLevel: $zulipFeatureLevel') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + late final $AccountsTable accounts = $AccountsTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [accounts]; +} diff --git a/test/model/database_test.dart b/test/model/database_test.dart new file mode 100644 index 0000000000..de0ef04dc4 --- /dev/null +++ b/test/model/database_test.dart @@ -0,0 +1,55 @@ +import 'package:checks/checks.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:test/scaffolding.dart'; +import 'package:zulip/model/database.dart'; + +void main() { + late AppDatabase database; + + setUp(() { + database = AppDatabase(NativeDatabase.memory()); + }); + tearDown(() async { + await database.close(); + }); + + test('create account', () async { + // TODO use example_data + final accountData = AccountsCompanion.insert( + realmUrl: Uri.parse('https://chat.example/'), + userId: 1, + email: 'asdf@example.org', + apiKey: '1234', + zulipVersion: '6.0', + zulipMergeBase: const Value('6.0'), + zulipFeatureLevel: 42, + ); + final accountId = await database.createAccount(accountData); + final account = await (database.select(database.accounts) + ..where((a) => a.id.equals(accountId))) + .watchSingle() + .first; + check(account.toCompanion(false).toJson()).deepEquals({ + ...accountData.toJson(), + 'id': it(), + }); + }); +} + +extension UpdateCompanionExtension on UpdateCompanion { + Map toJson() { + // Compare sketches of this idea in discussion at: + // https://github.com/simolus3/drift/issues/1924 + // To go upstream, this would need to handle DateTime + // and Uint8List variables, and would need a fromJson. + // Also should document that the keys are column names, + // not Dart field names. (The extension is on UpdateCompanion + // rather than Insertable to avoid confusion with the toJson + // on DataClass row classes, which use Dart field names.) + return { + for (final kv in toColumns(false).entries) + kv.key: (kv.value as Variable).value + }; + } +} From 9bd35bfd089548756a489084133c9e873a521a6a Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 10 Apr 2023 15:04:18 -0700 Subject: [PATCH 08/11] db: Set up for migrations in Drift --- .gitattributes | 4 + lib/model/database.dart | 35 ++- test/model/schemas/drift_schema_v1.json | 1 + test/model/schemas/schema.dart | 18 ++ test/model/schemas/schema_v1.dart | 353 ++++++++++++++++++++++++ 5 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 test/model/schemas/drift_schema_v1.json create mode 100644 test/model/schemas/schema.dart create mode 100644 test/model/schemas/schema_v1.dart diff --git a/.gitattributes b/.gitattributes index 64c84c5115..7cd77fdcc5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,10 @@ # Dart files generated from the files next to them: *.g.dart -diff +# Generated files for testing migrations: +test/model/schemas/*.dart -diff +test/model/schemas/*.json -diff + # On the other hand, keep diffs for pubspec.lock. It contains # information independent of any non-generated file in the tree. # And thankfully it's much less verbose than, say, a yarn.lock. diff --git a/lib/model/database.dart b/lib/model/database.dart index 5843325d47..9e94660dd3 100644 --- a/lib/model/database.dart +++ b/lib/model/database.dart @@ -49,8 +49,41 @@ class AppDatabase extends _$AppDatabase { AppDatabase.live() : this(_openConnection()); + // When updating the schema: + // * Make the change in the table classes, and bump schemaVersion. + // * Export the new schema: + // $ dart run drift_dev schema dump lib/model/database.dart test/model/schemas/ + // * Generate test migrations from the schemas: + // $ dart run drift_dev schema generate --data-classes --companions test/model/schemas/ test/model/schemas/ + // * Write a migration in `onUpgrade` below. + // * Write tests. + // TODO run those `drift_dev schema` commands in CI: https://github.com/zulip/zulip-flutter/issues/60 @override - int get schemaVersion => 1; // TODO migrations + int get schemaVersion => 1; // See note. + + @override + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (Migrator m) async { + await m.createAll(); + }, + onUpgrade: (Migrator m, int from, int to) async { + if (from > to) { + // TODO(log): log schema downgrade as an error + // This should only ever happen in dev. As a dev convenience, + // drop everything from the database and start over. + for (final entity in allSchemaEntities) { + // This will miss any entire tables (or indexes, etc.) that + // don't exist at this version. For a dev-only feature, that's OK. + await m.drop(entity); + } + await m.createAll(); + return; + } + assert(1 <= from && from <= to && to <= schemaVersion); + } + ); + } Future createAccount(AccountsCompanion values) { return into(accounts).insert(values); diff --git a/test/model/schemas/drift_schema_v1.json b/test/model/schemas/drift_schema_v1.json new file mode 100644 index 0000000000..3c3e9da0b8 --- /dev/null +++ b/test/model/schemas/drift_schema_v1.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"accounts","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"realm_url","getter_name":"realmUrl","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"api_key","getter_name":"apiKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_version","getter_name":"zulipVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_merge_base","getter_name":"zulipMergeBase","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_feature_level","getter_name":"zulipFeatureLevel","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"unique_keys":[["realm_url","user_id"],["realm_url","email"]]}}]} \ No newline at end of file diff --git a/test/model/schemas/schema.dart b/test/model/schemas/schema.dart new file mode 100644 index 0000000000..c9320642b0 --- /dev/null +++ b/test/model/schemas/schema.dart @@ -0,0 +1,18 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +//@dart=2.12 +import 'package:drift/drift.dart'; +import 'package:drift/internal/migrations.dart'; +import 'schema_v1.dart' as v1; + +class GeneratedHelper implements SchemaInstantiationHelper { + @override + GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { + switch (version) { + case 1: + return v1.DatabaseAtV1(db); + default: + throw MissingSchemaException(version, const {1}); + } + } +} diff --git a/test/model/schemas/schema_v1.dart b/test/model/schemas/schema_v1.dart new file mode 100644 index 0000000000..70900b8fbe --- /dev/null +++ b/test/model/schemas/schema_v1.dart @@ -0,0 +1,353 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +//@dart=2.12 +import 'package:drift/drift.dart'; + +class Accounts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Accounts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn realmUrl = GeneratedColumn( + 'realm_url', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn email = GeneratedColumn( + 'email', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn apiKey = GeneratedColumn( + 'api_key', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn zulipVersion = GeneratedColumn( + 'zulip_version', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn zulipMergeBase = GeneratedColumn( + 'zulip_merge_base', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn zulipFeatureLevel = GeneratedColumn( + 'zulip_feature_level', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [ + id, + realmUrl, + userId, + email, + apiKey, + zulipVersion, + zulipMergeBase, + zulipFeatureLevel + ]; + @override + String get aliasedName => _alias ?? 'accounts'; + @override + String get actualTableName => 'accounts'; + @override + Set get $primaryKey => {id}; + @override + List> get uniqueKeys => [ + {realmUrl, userId}, + {realmUrl, email}, + ]; + @override + AccountsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AccountsData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + realmUrl: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}realm_url'])!, + userId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}user_id'])!, + email: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}email'])!, + apiKey: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}api_key'])!, + zulipVersion: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}zulip_version'])!, + zulipMergeBase: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}zulip_merge_base']), + zulipFeatureLevel: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}zulip_feature_level'])!, + ); + } + + @override + Accounts createAlias(String alias) { + return Accounts(attachedDatabase, alias); + } +} + +class AccountsData extends DataClass implements Insertable { + final int id; + final String realmUrl; + final int userId; + final String email; + final String apiKey; + final String zulipVersion; + final String? zulipMergeBase; + final int zulipFeatureLevel; + const AccountsData( + {required this.id, + required this.realmUrl, + required this.userId, + required this.email, + required this.apiKey, + required this.zulipVersion, + this.zulipMergeBase, + required this.zulipFeatureLevel}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['realm_url'] = Variable(realmUrl); + map['user_id'] = Variable(userId); + map['email'] = Variable(email); + map['api_key'] = Variable(apiKey); + map['zulip_version'] = Variable(zulipVersion); + if (!nullToAbsent || zulipMergeBase != null) { + map['zulip_merge_base'] = Variable(zulipMergeBase); + } + map['zulip_feature_level'] = Variable(zulipFeatureLevel); + return map; + } + + AccountsCompanion toCompanion(bool nullToAbsent) { + return AccountsCompanion( + id: Value(id), + realmUrl: Value(realmUrl), + userId: Value(userId), + email: Value(email), + apiKey: Value(apiKey), + zulipVersion: Value(zulipVersion), + zulipMergeBase: zulipMergeBase == null && nullToAbsent + ? const Value.absent() + : Value(zulipMergeBase), + zulipFeatureLevel: Value(zulipFeatureLevel), + ); + } + + factory AccountsData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AccountsData( + id: serializer.fromJson(json['id']), + realmUrl: serializer.fromJson(json['realmUrl']), + userId: serializer.fromJson(json['userId']), + email: serializer.fromJson(json['email']), + apiKey: serializer.fromJson(json['apiKey']), + zulipVersion: serializer.fromJson(json['zulipVersion']), + zulipMergeBase: serializer.fromJson(json['zulipMergeBase']), + zulipFeatureLevel: serializer.fromJson(json['zulipFeatureLevel']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'realmUrl': serializer.toJson(realmUrl), + 'userId': serializer.toJson(userId), + 'email': serializer.toJson(email), + 'apiKey': serializer.toJson(apiKey), + 'zulipVersion': serializer.toJson(zulipVersion), + 'zulipMergeBase': serializer.toJson(zulipMergeBase), + 'zulipFeatureLevel': serializer.toJson(zulipFeatureLevel), + }; + } + + AccountsData copyWith( + {int? id, + String? realmUrl, + int? userId, + String? email, + String? apiKey, + String? zulipVersion, + Value zulipMergeBase = const Value.absent(), + int? zulipFeatureLevel}) => + AccountsData( + id: id ?? this.id, + realmUrl: realmUrl ?? this.realmUrl, + userId: userId ?? this.userId, + email: email ?? this.email, + apiKey: apiKey ?? this.apiKey, + zulipVersion: zulipVersion ?? this.zulipVersion, + zulipMergeBase: + zulipMergeBase.present ? zulipMergeBase.value : this.zulipMergeBase, + zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ); + @override + String toString() { + return (StringBuffer('AccountsData(') + ..write('id: $id, ') + ..write('realmUrl: $realmUrl, ') + ..write('userId: $userId, ') + ..write('email: $email, ') + ..write('apiKey: $apiKey, ') + ..write('zulipVersion: $zulipVersion, ') + ..write('zulipMergeBase: $zulipMergeBase, ') + ..write('zulipFeatureLevel: $zulipFeatureLevel') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, realmUrl, userId, email, apiKey, + zulipVersion, zulipMergeBase, zulipFeatureLevel); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AccountsData && + other.id == this.id && + other.realmUrl == this.realmUrl && + other.userId == this.userId && + other.email == this.email && + other.apiKey == this.apiKey && + other.zulipVersion == this.zulipVersion && + other.zulipMergeBase == this.zulipMergeBase && + other.zulipFeatureLevel == this.zulipFeatureLevel); +} + +class AccountsCompanion extends UpdateCompanion { + final Value id; + final Value realmUrl; + final Value userId; + final Value email; + final Value apiKey; + final Value zulipVersion; + final Value zulipMergeBase; + final Value zulipFeatureLevel; + const AccountsCompanion({ + this.id = const Value.absent(), + this.realmUrl = const Value.absent(), + this.userId = const Value.absent(), + this.email = const Value.absent(), + this.apiKey = const Value.absent(), + this.zulipVersion = const Value.absent(), + this.zulipMergeBase = const Value.absent(), + this.zulipFeatureLevel = const Value.absent(), + }); + AccountsCompanion.insert({ + this.id = const Value.absent(), + required String realmUrl, + required int userId, + required String email, + required String apiKey, + required String zulipVersion, + this.zulipMergeBase = const Value.absent(), + required int zulipFeatureLevel, + }) : realmUrl = Value(realmUrl), + userId = Value(userId), + email = Value(email), + apiKey = Value(apiKey), + zulipVersion = Value(zulipVersion), + zulipFeatureLevel = Value(zulipFeatureLevel); + static Insertable custom({ + Expression? id, + Expression? realmUrl, + Expression? userId, + Expression? email, + Expression? apiKey, + Expression? zulipVersion, + Expression? zulipMergeBase, + Expression? zulipFeatureLevel, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (realmUrl != null) 'realm_url': realmUrl, + if (userId != null) 'user_id': userId, + if (email != null) 'email': email, + if (apiKey != null) 'api_key': apiKey, + if (zulipVersion != null) 'zulip_version': zulipVersion, + if (zulipMergeBase != null) 'zulip_merge_base': zulipMergeBase, + if (zulipFeatureLevel != null) 'zulip_feature_level': zulipFeatureLevel, + }); + } + + AccountsCompanion copyWith( + {Value? id, + Value? realmUrl, + Value? userId, + Value? email, + Value? apiKey, + Value? zulipVersion, + Value? zulipMergeBase, + Value? zulipFeatureLevel}) { + return AccountsCompanion( + id: id ?? this.id, + realmUrl: realmUrl ?? this.realmUrl, + userId: userId ?? this.userId, + email: email ?? this.email, + apiKey: apiKey ?? this.apiKey, + zulipVersion: zulipVersion ?? this.zulipVersion, + zulipMergeBase: zulipMergeBase ?? this.zulipMergeBase, + zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (realmUrl.present) { + map['realm_url'] = Variable(realmUrl.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (apiKey.present) { + map['api_key'] = Variable(apiKey.value); + } + if (zulipVersion.present) { + map['zulip_version'] = Variable(zulipVersion.value); + } + if (zulipMergeBase.present) { + map['zulip_merge_base'] = Variable(zulipMergeBase.value); + } + if (zulipFeatureLevel.present) { + map['zulip_feature_level'] = Variable(zulipFeatureLevel.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AccountsCompanion(') + ..write('id: $id, ') + ..write('realmUrl: $realmUrl, ') + ..write('userId: $userId, ') + ..write('email: $email, ') + ..write('apiKey: $apiKey, ') + ..write('zulipVersion: $zulipVersion, ') + ..write('zulipMergeBase: $zulipMergeBase, ') + ..write('zulipFeatureLevel: $zulipFeatureLevel') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV1 extends GeneratedDatabase { + DatabaseAtV1(QueryExecutor e) : super(e); + late final Accounts accounts = Accounts(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [accounts]; + @override + int get schemaVersion => 1; +} From 62c332f8693f2ec62369aaeaa883a609cfed0637 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 17 Apr 2023 12:21:51 -0700 Subject: [PATCH 09/11] store [nfc]: Use Account type generated for database This will keep things simple when we start storing these in the database. --- lib/model/database.dart | 1 - lib/model/store.dart | 51 ++++++++++++++++---------------------- lib/widgets/login.dart | 9 +++---- test/example_data.dart | 2 ++ test/model/store_test.dart | 13 ++++++++-- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/lib/model/database.dart b/lib/model/database.dart index 9e94660dd3..14b2189136 100644 --- a/lib/model/database.dart +++ b/lib/model/database.dart @@ -7,7 +7,6 @@ import 'package:path_provider/path_provider.dart'; part 'database.g.dart'; -// TODO unify with Account in store.dart class Accounts extends Table { Column get id => integer().autoIncrement()(); diff --git a/lib/model/store.dart b/lib/model/store.dart index a66b4cb0d7..b12f1d7c0e 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -9,8 +9,12 @@ import '../api/model/model.dart'; import '../api/route/events.dart'; import '../api/route/messages.dart'; import '../credential_fixture.dart' as credentials; +import 'database.dart'; import 'message_list.dart'; +export 'package:drift/drift.dart' show Value; +export 'database.dart' show Account, AccountsCompanion; + /// Store for all the user's data. /// /// From UI code, use [GlobalStoreWidget.of] to get hold of an appropriate @@ -109,16 +113,16 @@ abstract class GlobalStore extends ChangeNotifier { // TODO(#13): rewrite these setters/mutators with a database /// Add an account to the store, returning its assigned account ID. - Future insertAccount(Account account) async { - final accountId = await doInsertAccount(account); - assert(!_accounts.containsKey(accountId)); - _accounts[accountId] = account; + Future insertAccount(AccountsCompanion data) async { + final account = await doInsertAccount(data); + assert(!_accounts.containsKey(account.id)); + _accounts[account.id] = account; notifyListeners(); - return accountId; + return account.id; } /// Add an account to the underlying data store. - Future doInsertAccount(Account account); + Future doInsertAccount(AccountsCompanion data); // More mutators as needed: // Future updateAccount... @@ -203,27 +207,6 @@ class PerAccountStore extends ChangeNotifier { } } -@immutable -class Account { - const Account({ - required this.realmUrl, - required this.userId, - required this.email, - required this.apiKey, - required this.zulipFeatureLevel, - required this.zulipVersion, - required this.zulipMergeBase, - }); - - final Uri realmUrl; - final int userId; - final String email; - final String apiKey; - final int zulipFeatureLevel; - final String zulipVersion; - final String? zulipMergeBase; -} - class LiveGlobalStore extends GlobalStore { LiveGlobalStore._({required super.accounts}) : super(); @@ -245,10 +228,19 @@ class LiveGlobalStore extends GlobalStore { int _nextAccountId = 1; @override - Future doInsertAccount(Account account) async { + Future doInsertAccount(AccountsCompanion data) async { final accountId = _nextAccountId; _nextAccountId++; - return accountId; + return Account( + id: accountId, + realmUrl: data.realmUrl.value, + userId: data.userId.value, + email: data.email.value, + apiKey: data.apiKey.value, + zulipFeatureLevel: data.zulipFeatureLevel.value, + zulipVersion: data.zulipVersion.value, + zulipMergeBase: data.zulipMergeBase.value, + ); } } @@ -257,6 +249,7 @@ class LiveGlobalStore extends GlobalStore { /// See "Server credentials" in the project README for how to fill in the /// `credential_fixture.dart` file this requires. final Account _fixtureAccount = Account( + id: LiveGlobalStore.fixtureAccountId, realmUrl: Uri.parse(credentials.realmUrl), email: credentials.email, apiKey: credentials.apiKey, diff --git a/lib/widgets/login.dart b/lib/widgets/login.dart index f85c4ed2b1..c2f5038949 100644 --- a/lib/widgets/login.dart +++ b/lib/widgets/login.dart @@ -135,17 +135,16 @@ class _EmailPasswordLoginPageState extends State { return; } - final account = Account( + final globalStore = GlobalStoreWidget.of(context); + final accountId = await globalStore.insertAccount(AccountsCompanion.insert( realmUrl: realmUrl, email: result.email, apiKey: result.apiKey, userId: userId, zulipFeatureLevel: widget.serverSettings.zulipFeatureLevel, zulipVersion: widget.serverSettings.zulipVersion, - zulipMergeBase: widget.serverSettings.zulipMergeBase, - ); - final globalStore = GlobalStoreWidget.of(context); - final accountId = await globalStore.insertAccount(account); + zulipMergeBase: Value(widget.serverSettings.zulipMergeBase), + )); if (context.mounted) {} // https://github.com/dart-lang/linter/issues/4007 else { return; diff --git a/test/example_data.dart b/test/example_data.dart index 6332b4c4cc..1e6365d696 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -8,6 +8,7 @@ const String recentZulipVersion = '6.1'; const int recentZulipFeatureLevel = 164; final Account selfAccount = Account( + id: 1001, realmUrl: realmUrl, email: 'self@example', apiKey: 'asdfqwer', @@ -18,6 +19,7 @@ final Account selfAccount = Account( ); final Account otherAccount = Account( + id: 1002, realmUrl: realmUrl, email: 'other@example', apiKey: 'sdfgwert', diff --git a/test/model/store_test.dart b/test/model/store_test.dart index cd09ee0fdd..409318f91d 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -113,9 +113,18 @@ class TestGlobalStore extends GlobalStore { int _nextAccountId = 1; @override - Future doInsertAccount(Account account) async { + Future doInsertAccount(AccountsCompanion data) async { final accountId = _nextAccountId; _nextAccountId++; - return accountId; + return Account( + id: accountId, + realmUrl: data.realmUrl.value, + userId: data.userId.value, + email: data.email.value, + apiKey: data.apiKey.value, + zulipFeatureLevel: data.zulipFeatureLevel.value, + zulipVersion: data.zulipVersion.value, + zulipMergeBase: data.zulipMergeBase.value, + ); } } From ea14c73e4b9694df767810aa6f1bb7a37742f270 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 17 Apr 2023 12:40:13 -0700 Subject: [PATCH 10/11] store: Keep login credentials in the database Fixes: #13 Fixes: #34 --- .gitignore | 2 +- README.md | 33 ---------------- lib/model/store.dart | 89 ++++++++++++++++++++++++------------------ lib/widgets/login.dart | 1 + 4 files changed, 53 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index c997bb5a1d..eda38803a1 100644 --- a/.gitignore +++ b/.gitignore @@ -43,5 +43,5 @@ app.*.map.json /android/app/profile /android/app/release -# Scaffolding hack +# Old scaffolding hack lib/credential_fixture.dart diff --git a/README.md b/README.md index 8b534358b7..e9bfd3bd4b 100644 --- a/README.md +++ b/README.md @@ -39,39 +39,6 @@ community. See [issue #15][]. [issue #15]: https://github.com/zulip/zulip-flutter/issues/15 -### Server credentials - -In this early prototype, we don't yet have a UI for logging into -a Zulip server. Instead, you supply Zulip credentials at build time. - -To do this, log into the Zulip web app for the test account -you want to use, and gather two kinds of information: -* [Download a `.zuliprc` file][download-zuliprc]. - This will contain a realm URL, email, and API key. -* Find the account's user ID. You can do this by visiting your - DMs with yourself, and looking at the URL; - it's the number after `pm-with/` or `dm-with/`. - -Then create a file `lib/credential_fixture.dart` in this worktree -with the following form, and fill in the gathered information: -```dart -const String realmUrl = '…'; -const String email = '…'; -const String apiKey = '…'; -const int userId = /* … */ -1; -``` - -Now build and run the app (see "Flutter help" above), and things -should work. - -Note this means the account's API key gets incorporated into the -build output. Consider using a low-value test account, or else -deleting the build output (`flutter clean`, and then delete the app -from any mobile devices you ran it on) when done. - -[download-zuliprc]: https://zulip.com/api/api-keys - - ### Tests You can run all our forms of tests with two commands: diff --git a/lib/model/store.dart b/lib/model/store.dart index b12f1d7c0e..fe86f414f5 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -1,6 +1,10 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import '../api/core.dart'; import '../api/model/events.dart'; @@ -8,7 +12,6 @@ import '../api/model/initial_snapshot.dart'; import '../api/model/model.dart'; import '../api/route/events.dart'; import '../api/route/messages.dart'; -import '../credential_fixture.dart' as credentials; import 'database.dart'; import 'message_list.dart'; @@ -110,8 +113,6 @@ abstract class GlobalStore extends ChangeNotifier { Account? getAccount(int id) => _accounts[id]; - // TODO(#13): rewrite these setters/mutators with a database - /// Add an account to the store, returning its assigned account ID. Future insertAccount(AccountsCompanion data) async { final account = await doInsertAccount(data); @@ -207,58 +208,70 @@ class PerAccountStore extends ChangeNotifier { } } +/// A [GlobalStore] that uses a live server and live, persistent local database. +/// +/// The underlying data store is an [AppDatabase] corresponding to a SQLite +/// database file in the app's persistent storage on the device. +/// +/// The per-account stores will be instances of [LivePerAccountStore], +/// with data loaded through [LiveApiConnection]. class LiveGlobalStore extends GlobalStore { - LiveGlobalStore._({required super.accounts}) : super(); - - // For convenience, a number we won't use as an ID in the database table. - static const fixtureAccountId = -1; + LiveGlobalStore._({ + required AppDatabase db, + required super.accounts, + }) : _db = db; // We keep the API simple and synchronous for the bulk of the app's code // by doing this loading up front before constructing a [GlobalStore]. static Future load() async { - final accounts = {fixtureAccountId: _fixtureAccount}; - return LiveGlobalStore._(accounts: accounts); + final db = AppDatabase(NativeDatabase.createInBackground(await _dbFile())); + final accounts = await db.select(db.accounts).get(); + return LiveGlobalStore._( + db: db, + accounts: Map.fromEntries(accounts.map((a) => MapEntry(a.id, a))), + ); + } + + /// The file path to use for the app database. + static Future _dbFile() async { + // What directory should we use? + // path_provider's getApplicationSupportDirectory: + // on Android, -> Flutter's PathUtils.getFilesDir -> https://developer.android.com/reference/android/content/Context#getFilesDir() + // -> empirically /data/data/com.zulip.flutter/files/ + // on iOS, -> "Library/Application Support" via https://developer.apple.com/documentation/foundation/nssearchpathdirectory/nsapplicationsupportdirectory + // on Linux, -> "${XDG_DATA_HOME:-~/.local/share}/com.zulip.flutter/" + // All seem reasonable. + // path_provider's getApplicationDocumentsDirectory: + // on Android, -> Flutter's PathUtils.getDataDirectory -> https://developer.android.com/reference/android/content/Context#getDir(java.lang.String,%20int) + // with https://developer.android.com/reference/android/content/Context#MODE_PRIVATE + // on iOS, "Document directory" via https://developer.apple.com/documentation/foundation/nssearchpathdirectory/nsdocumentdirectory + // on Linux, -> `xdg-user-dir DOCUMENTS` -> e.g. ~/Documents + // That Linux answer is definitely not a fit. Harder to tell about the rest. + final dir = await getApplicationSupportDirectory(); + return File(p.join(dir.path, 'zulip.db')); } + final AppDatabase _db; + @override Future loadPerAccount(Account account) { return LivePerAccountStore.load(account); } - int _nextAccountId = 1; - @override Future doInsertAccount(AccountsCompanion data) async { - final accountId = _nextAccountId; - _nextAccountId++; - return Account( - id: accountId, - realmUrl: data.realmUrl.value, - userId: data.userId.value, - email: data.email.value, - apiKey: data.apiKey.value, - zulipFeatureLevel: data.zulipFeatureLevel.value, - zulipVersion: data.zulipVersion.value, - zulipMergeBase: data.zulipMergeBase.value, - ); + final accountId = await _db.createAccount(data); // TODO(log): db errors + // We can *basically* predict what the Account will contain + // based on the AccountsCompanion and the account ID. But + // if we did that and then there was some subtle case where we + // didn't match the database's behavior, that'd be a nasty bug. + // This isn't a hot path, so just make the extra query. + return await (_db.select(_db.accounts) // TODO perhaps put this logic in AppDatabase + ..where((a) => a.id.equals(accountId)) + ).getSingle(); } } -/// A scaffolding hack for while prototyping. -/// -/// See "Server credentials" in the project README for how to fill in the -/// `credential_fixture.dart` file this requires. -final Account _fixtureAccount = Account( - id: LiveGlobalStore.fixtureAccountId, - realmUrl: Uri.parse(credentials.realmUrl), - email: credentials.email, - apiKey: credentials.apiKey, - userId: credentials.userId, - zulipFeatureLevel: 169, - zulipVersion: '6.0-1235-g061f1dc43b', - zulipMergeBase: '6.0-1235-g061f1dc43b', -); - /// A [PerAccountStore] which polls an event queue to stay up to date. class LivePerAccountStore extends PerAccountStore { LivePerAccountStore.fromInitialSnapshot({ diff --git a/lib/widgets/login.dart b/lib/widgets/login.dart index c2f5038949..f68e8398d5 100644 --- a/lib/widgets/login.dart +++ b/lib/widgets/login.dart @@ -136,6 +136,7 @@ class _EmailPasswordLoginPageState extends State { } final globalStore = GlobalStoreWidget.of(context); + // TODO(#35): give feedback to user on SQL exception, like dupe realm+user final accountId = await globalStore.insertAccount(AccountsCompanion.insert( realmUrl: realmUrl, email: result.email, From 066fe259a15c3dad45ee912b0d750c42a094648b Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 27 Feb 2023 20:13:39 -0800 Subject: [PATCH 11/11] db: Add a migration, and a migration test We didn't really need to do this as a migration, because we already know we need this column and so we could have included it from the start. But this lets us exercise Drift's support for migrations, and for testing migrations. --- lib/model/database.dart | 9 +- lib/model/database.g.dart | 57 +++- test/model/database_test.dart | 99 ++++-- test/model/schemas/drift_schema_v2.json | 1 + test/model/schemas/schema.dart | 5 +- test/model/schemas/schema_v2.dart | 385 ++++++++++++++++++++++++ 6 files changed, 521 insertions(+), 35 deletions(-) create mode 100644 test/model/schemas/drift_schema_v2.json create mode 100644 test/model/schemas/schema_v2.dart diff --git a/lib/model/database.dart b/lib/model/database.dart index 14b2189136..5bc257b7f5 100644 --- a/lib/model/database.dart +++ b/lib/model/database.dart @@ -20,6 +20,8 @@ class Accounts extends Table { Column get zulipMergeBase => text().nullable()(); Column get zulipFeatureLevel => integer()(); + Column get ackedPushToken => text().nullable()(); + @override List>> get uniqueKeys => [ {realmUrl, userId}, @@ -58,7 +60,7 @@ class AppDatabase extends _$AppDatabase { // * Write tests. // TODO run those `drift_dev schema` commands in CI: https://github.com/zulip/zulip-flutter/issues/60 @override - int get schemaVersion => 1; // See note. + int get schemaVersion => 2; // See note. @override MigrationStrategy get migration { @@ -80,6 +82,11 @@ class AppDatabase extends _$AppDatabase { return; } assert(1 <= from && from <= to && to <= schemaVersion); + + if (from < 2 && 2 <= to) { + await m.addColumn(accounts, accounts.ackedPushToken); + } + // New migrations go here. } ); } diff --git a/lib/model/database.g.dart b/lib/model/database.g.dart index ea54de5356..613c5919ae 100644 --- a/lib/model/database.g.dart +++ b/lib/model/database.g.dart @@ -57,6 +57,12 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { late final GeneratedColumn zulipFeatureLevel = GeneratedColumn( 'zulip_feature_level', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _ackedPushTokenMeta = + const VerificationMeta('ackedPushToken'); + @override + late final GeneratedColumn ackedPushToken = GeneratedColumn( + 'acked_push_token', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); @override List get $columns => [ id, @@ -66,7 +72,8 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { apiKey, zulipVersion, zulipMergeBase, - zulipFeatureLevel + zulipFeatureLevel, + ackedPushToken ]; @override String get aliasedName => _alias ?? 'accounts'; @@ -121,6 +128,12 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { } else if (isInserting) { context.missing(_zulipFeatureLevelMeta); } + if (data.containsKey('acked_push_token')) { + context.handle( + _ackedPushTokenMeta, + ackedPushToken.isAcceptableOrUnknown( + data['acked_push_token']!, _ackedPushTokenMeta)); + } return context; } @@ -152,6 +165,8 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { DriftSqlType.string, data['${effectivePrefix}zulip_merge_base']), zulipFeatureLevel: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}zulip_feature_level'])!, + ackedPushToken: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}acked_push_token']), ); } @@ -172,6 +187,7 @@ class Account extends DataClass implements Insertable { final String zulipVersion; final String? zulipMergeBase; final int zulipFeatureLevel; + final String? ackedPushToken; const Account( {required this.id, required this.realmUrl, @@ -180,7 +196,8 @@ class Account extends DataClass implements Insertable { required this.apiKey, required this.zulipVersion, this.zulipMergeBase, - required this.zulipFeatureLevel}); + required this.zulipFeatureLevel, + this.ackedPushToken}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -197,6 +214,9 @@ class Account extends DataClass implements Insertable { map['zulip_merge_base'] = Variable(zulipMergeBase); } map['zulip_feature_level'] = Variable(zulipFeatureLevel); + if (!nullToAbsent || ackedPushToken != null) { + map['acked_push_token'] = Variable(ackedPushToken); + } return map; } @@ -212,6 +232,9 @@ class Account extends DataClass implements Insertable { ? const Value.absent() : Value(zulipMergeBase), zulipFeatureLevel: Value(zulipFeatureLevel), + ackedPushToken: ackedPushToken == null && nullToAbsent + ? const Value.absent() + : Value(ackedPushToken), ); } @@ -227,6 +250,7 @@ class Account extends DataClass implements Insertable { zulipVersion: serializer.fromJson(json['zulipVersion']), zulipMergeBase: serializer.fromJson(json['zulipMergeBase']), zulipFeatureLevel: serializer.fromJson(json['zulipFeatureLevel']), + ackedPushToken: serializer.fromJson(json['ackedPushToken']), ); } @override @@ -241,6 +265,7 @@ class Account extends DataClass implements Insertable { 'zulipVersion': serializer.toJson(zulipVersion), 'zulipMergeBase': serializer.toJson(zulipMergeBase), 'zulipFeatureLevel': serializer.toJson(zulipFeatureLevel), + 'ackedPushToken': serializer.toJson(ackedPushToken), }; } @@ -252,7 +277,8 @@ class Account extends DataClass implements Insertable { String? apiKey, String? zulipVersion, Value zulipMergeBase = const Value.absent(), - int? zulipFeatureLevel}) => + int? zulipFeatureLevel, + Value ackedPushToken = const Value.absent()}) => Account( id: id ?? this.id, realmUrl: realmUrl ?? this.realmUrl, @@ -263,6 +289,8 @@ class Account extends DataClass implements Insertable { zulipMergeBase: zulipMergeBase.present ? zulipMergeBase.value : this.zulipMergeBase, zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ackedPushToken: + ackedPushToken.present ? ackedPushToken.value : this.ackedPushToken, ); @override String toString() { @@ -274,14 +302,15 @@ class Account extends DataClass implements Insertable { ..write('apiKey: $apiKey, ') ..write('zulipVersion: $zulipVersion, ') ..write('zulipMergeBase: $zulipMergeBase, ') - ..write('zulipFeatureLevel: $zulipFeatureLevel') + ..write('zulipFeatureLevel: $zulipFeatureLevel, ') + ..write('ackedPushToken: $ackedPushToken') ..write(')')) .toString(); } @override int get hashCode => Object.hash(id, realmUrl, userId, email, apiKey, - zulipVersion, zulipMergeBase, zulipFeatureLevel); + zulipVersion, zulipMergeBase, zulipFeatureLevel, ackedPushToken); @override bool operator ==(Object other) => identical(this, other) || @@ -293,7 +322,8 @@ class Account extends DataClass implements Insertable { other.apiKey == this.apiKey && other.zulipVersion == this.zulipVersion && other.zulipMergeBase == this.zulipMergeBase && - other.zulipFeatureLevel == this.zulipFeatureLevel); + other.zulipFeatureLevel == this.zulipFeatureLevel && + other.ackedPushToken == this.ackedPushToken); } class AccountsCompanion extends UpdateCompanion { @@ -305,6 +335,7 @@ class AccountsCompanion extends UpdateCompanion { final Value zulipVersion; final Value zulipMergeBase; final Value zulipFeatureLevel; + final Value ackedPushToken; const AccountsCompanion({ this.id = const Value.absent(), this.realmUrl = const Value.absent(), @@ -314,6 +345,7 @@ class AccountsCompanion extends UpdateCompanion { this.zulipVersion = const Value.absent(), this.zulipMergeBase = const Value.absent(), this.zulipFeatureLevel = const Value.absent(), + this.ackedPushToken = const Value.absent(), }); AccountsCompanion.insert({ this.id = const Value.absent(), @@ -324,6 +356,7 @@ class AccountsCompanion extends UpdateCompanion { required String zulipVersion, this.zulipMergeBase = const Value.absent(), required int zulipFeatureLevel, + this.ackedPushToken = const Value.absent(), }) : realmUrl = Value(realmUrl), userId = Value(userId), email = Value(email), @@ -339,6 +372,7 @@ class AccountsCompanion extends UpdateCompanion { Expression? zulipVersion, Expression? zulipMergeBase, Expression? zulipFeatureLevel, + Expression? ackedPushToken, }) { return RawValuesInsertable({ if (id != null) 'id': id, @@ -349,6 +383,7 @@ class AccountsCompanion extends UpdateCompanion { if (zulipVersion != null) 'zulip_version': zulipVersion, if (zulipMergeBase != null) 'zulip_merge_base': zulipMergeBase, if (zulipFeatureLevel != null) 'zulip_feature_level': zulipFeatureLevel, + if (ackedPushToken != null) 'acked_push_token': ackedPushToken, }); } @@ -360,7 +395,8 @@ class AccountsCompanion extends UpdateCompanion { Value? apiKey, Value? zulipVersion, Value? zulipMergeBase, - Value? zulipFeatureLevel}) { + Value? zulipFeatureLevel, + Value? ackedPushToken}) { return AccountsCompanion( id: id ?? this.id, realmUrl: realmUrl ?? this.realmUrl, @@ -370,6 +406,7 @@ class AccountsCompanion extends UpdateCompanion { zulipVersion: zulipVersion ?? this.zulipVersion, zulipMergeBase: zulipMergeBase ?? this.zulipMergeBase, zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ackedPushToken: ackedPushToken ?? this.ackedPushToken, ); } @@ -401,6 +438,9 @@ class AccountsCompanion extends UpdateCompanion { if (zulipFeatureLevel.present) { map['zulip_feature_level'] = Variable(zulipFeatureLevel.value); } + if (ackedPushToken.present) { + map['acked_push_token'] = Variable(ackedPushToken.value); + } return map; } @@ -414,7 +454,8 @@ class AccountsCompanion extends UpdateCompanion { ..write('apiKey: $apiKey, ') ..write('zulipVersion: $zulipVersion, ') ..write('zulipMergeBase: $zulipMergeBase, ') - ..write('zulipFeatureLevel: $zulipFeatureLevel') + ..write('zulipFeatureLevel: $zulipFeatureLevel, ') + ..write('ackedPushToken: $ackedPushToken') ..write(')')) .toString(); } diff --git a/test/model/database_test.dart b/test/model/database_test.dart index de0ef04dc4..ecd9b66859 100644 --- a/test/model/database_test.dart +++ b/test/model/database_test.dart @@ -1,38 +1,87 @@ import 'package:checks/checks.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; +import 'package:drift_dev/api/migrations.dart'; import 'package:test/scaffolding.dart'; import 'package:zulip/model/database.dart'; +import 'schemas/schema.dart'; +import 'schemas/schema_v1.dart' as v1; +import 'schemas/schema_v2.dart' as v2; + void main() { - late AppDatabase database; + group('non-migration tests', () { + late AppDatabase database; - setUp(() { - database = AppDatabase(NativeDatabase.memory()); - }); - tearDown(() async { - await database.close(); + setUp(() { + database = AppDatabase(NativeDatabase.memory()); + }); + tearDown(() async { + await database.close(); + }); + + test('create account', () async { + final accountData = AccountsCompanion.insert( + realmUrl: Uri.parse('https://chat.example/'), + userId: 1, + email: 'asdf@example.org', + apiKey: '1234', + zulipVersion: '6.0', + zulipMergeBase: const Value('6.0'), + zulipFeatureLevel: 42, + ); + final accountId = await database.createAccount(accountData); + final account = await (database.select(database.accounts) + ..where((a) => a.id.equals(accountId))) + .watchSingle() + .first; + check(account.toCompanion(false).toJson()).deepEquals({ + ...accountData.toJson(), + 'id': it(), + 'acked_push_token': null, + }); + }); }); - test('create account', () async { - // TODO use example_data - final accountData = AccountsCompanion.insert( - realmUrl: Uri.parse('https://chat.example/'), - userId: 1, - email: 'asdf@example.org', - apiKey: '1234', - zulipVersion: '6.0', - zulipMergeBase: const Value('6.0'), - zulipFeatureLevel: 42, - ); - final accountId = await database.createAccount(accountData); - final account = await (database.select(database.accounts) - ..where((a) => a.id.equals(accountId))) - .watchSingle() - .first; - check(account.toCompanion(false).toJson()).deepEquals({ - ...accountData.toJson(), - 'id': it(), + group('migrations', () { + late SchemaVerifier verifier; + + setUpAll(() { + verifier = SchemaVerifier(GeneratedHelper()); + }); + + test('upgrade to v2, empty', () async { + final connection = await verifier.startAt(1); + final db = AppDatabase(connection); + await verifier.migrateAndValidate(db, 2); + await db.close(); + }); + + test('upgrade to v2, with data', () async { + final schema = await verifier.schemaAt(1); + final before = v1.DatabaseAtV1(schema.newConnection()); + await before.into(before.accounts).insert(v1.AccountsCompanion.insert( + realmUrl: 'https://chat.example/', + userId: 1, + email: 'asdf@example.org', + apiKey: '1234', + zulipVersion: '6.0', + zulipMergeBase: const Value('6.0'), + zulipFeatureLevel: 42, + )); + final accountV1 = await before.select(before.accounts).watchSingle().first; + await before.close(); + + final db = AppDatabase(schema.newConnection()); + await verifier.migrateAndValidate(db, 2); + await db.close(); + + final after = v2.DatabaseAtV2(schema.newConnection()); + final account = await after.select(after.accounts).getSingle(); + check(account.toJson()).deepEquals({ + ...accountV1.toJson(), + 'ackedPushToken': null, + }); }); }); } diff --git a/test/model/schemas/drift_schema_v2.json b/test/model/schemas/drift_schema_v2.json new file mode 100644 index 0000000000..4d639ddead --- /dev/null +++ b/test/model/schemas/drift_schema_v2.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"accounts","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"realm_url","getter_name":"realmUrl","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"api_key","getter_name":"apiKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_version","getter_name":"zulipVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_merge_base","getter_name":"zulipMergeBase","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_feature_level","getter_name":"zulipFeatureLevel","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"acked_push_token","getter_name":"ackedPushToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"unique_keys":[["realm_url","user_id"],["realm_url","email"]]}}]} \ No newline at end of file diff --git a/test/model/schemas/schema.dart b/test/model/schemas/schema.dart index c9320642b0..c8c6ff5926 100644 --- a/test/model/schemas/schema.dart +++ b/test/model/schemas/schema.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; import 'schema_v1.dart' as v1; +import 'schema_v2.dart' as v2; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -11,8 +12,10 @@ class GeneratedHelper implements SchemaInstantiationHelper { switch (version) { case 1: return v1.DatabaseAtV1(db); + case 2: + return v2.DatabaseAtV2(db); default: - throw MissingSchemaException(version, const {1}); + throw MissingSchemaException(version, const {1, 2}); } } } diff --git a/test/model/schemas/schema_v2.dart b/test/model/schemas/schema_v2.dart new file mode 100644 index 0000000000..55655e8976 --- /dev/null +++ b/test/model/schemas/schema_v2.dart @@ -0,0 +1,385 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +//@dart=2.12 +import 'package:drift/drift.dart'; + +class Accounts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Accounts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn realmUrl = GeneratedColumn( + 'realm_url', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn email = GeneratedColumn( + 'email', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn apiKey = GeneratedColumn( + 'api_key', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn zulipVersion = GeneratedColumn( + 'zulip_version', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn zulipMergeBase = GeneratedColumn( + 'zulip_merge_base', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn zulipFeatureLevel = GeneratedColumn( + 'zulip_feature_level', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn ackedPushToken = GeneratedColumn( + 'acked_push_token', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + id, + realmUrl, + userId, + email, + apiKey, + zulipVersion, + zulipMergeBase, + zulipFeatureLevel, + ackedPushToken + ]; + @override + String get aliasedName => _alias ?? 'accounts'; + @override + String get actualTableName => 'accounts'; + @override + Set get $primaryKey => {id}; + @override + List> get uniqueKeys => [ + {realmUrl, userId}, + {realmUrl, email}, + ]; + @override + AccountsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AccountsData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + realmUrl: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}realm_url'])!, + userId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}user_id'])!, + email: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}email'])!, + apiKey: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}api_key'])!, + zulipVersion: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}zulip_version'])!, + zulipMergeBase: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}zulip_merge_base']), + zulipFeatureLevel: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}zulip_feature_level'])!, + ackedPushToken: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}acked_push_token']), + ); + } + + @override + Accounts createAlias(String alias) { + return Accounts(attachedDatabase, alias); + } +} + +class AccountsData extends DataClass implements Insertable { + final int id; + final String realmUrl; + final int userId; + final String email; + final String apiKey; + final String zulipVersion; + final String? zulipMergeBase; + final int zulipFeatureLevel; + final String? ackedPushToken; + const AccountsData( + {required this.id, + required this.realmUrl, + required this.userId, + required this.email, + required this.apiKey, + required this.zulipVersion, + this.zulipMergeBase, + required this.zulipFeatureLevel, + this.ackedPushToken}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['realm_url'] = Variable(realmUrl); + map['user_id'] = Variable(userId); + map['email'] = Variable(email); + map['api_key'] = Variable(apiKey); + map['zulip_version'] = Variable(zulipVersion); + if (!nullToAbsent || zulipMergeBase != null) { + map['zulip_merge_base'] = Variable(zulipMergeBase); + } + map['zulip_feature_level'] = Variable(zulipFeatureLevel); + if (!nullToAbsent || ackedPushToken != null) { + map['acked_push_token'] = Variable(ackedPushToken); + } + return map; + } + + AccountsCompanion toCompanion(bool nullToAbsent) { + return AccountsCompanion( + id: Value(id), + realmUrl: Value(realmUrl), + userId: Value(userId), + email: Value(email), + apiKey: Value(apiKey), + zulipVersion: Value(zulipVersion), + zulipMergeBase: zulipMergeBase == null && nullToAbsent + ? const Value.absent() + : Value(zulipMergeBase), + zulipFeatureLevel: Value(zulipFeatureLevel), + ackedPushToken: ackedPushToken == null && nullToAbsent + ? const Value.absent() + : Value(ackedPushToken), + ); + } + + factory AccountsData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AccountsData( + id: serializer.fromJson(json['id']), + realmUrl: serializer.fromJson(json['realmUrl']), + userId: serializer.fromJson(json['userId']), + email: serializer.fromJson(json['email']), + apiKey: serializer.fromJson(json['apiKey']), + zulipVersion: serializer.fromJson(json['zulipVersion']), + zulipMergeBase: serializer.fromJson(json['zulipMergeBase']), + zulipFeatureLevel: serializer.fromJson(json['zulipFeatureLevel']), + ackedPushToken: serializer.fromJson(json['ackedPushToken']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'realmUrl': serializer.toJson(realmUrl), + 'userId': serializer.toJson(userId), + 'email': serializer.toJson(email), + 'apiKey': serializer.toJson(apiKey), + 'zulipVersion': serializer.toJson(zulipVersion), + 'zulipMergeBase': serializer.toJson(zulipMergeBase), + 'zulipFeatureLevel': serializer.toJson(zulipFeatureLevel), + 'ackedPushToken': serializer.toJson(ackedPushToken), + }; + } + + AccountsData copyWith( + {int? id, + String? realmUrl, + int? userId, + String? email, + String? apiKey, + String? zulipVersion, + Value zulipMergeBase = const Value.absent(), + int? zulipFeatureLevel, + Value ackedPushToken = const Value.absent()}) => + AccountsData( + id: id ?? this.id, + realmUrl: realmUrl ?? this.realmUrl, + userId: userId ?? this.userId, + email: email ?? this.email, + apiKey: apiKey ?? this.apiKey, + zulipVersion: zulipVersion ?? this.zulipVersion, + zulipMergeBase: + zulipMergeBase.present ? zulipMergeBase.value : this.zulipMergeBase, + zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ackedPushToken: + ackedPushToken.present ? ackedPushToken.value : this.ackedPushToken, + ); + @override + String toString() { + return (StringBuffer('AccountsData(') + ..write('id: $id, ') + ..write('realmUrl: $realmUrl, ') + ..write('userId: $userId, ') + ..write('email: $email, ') + ..write('apiKey: $apiKey, ') + ..write('zulipVersion: $zulipVersion, ') + ..write('zulipMergeBase: $zulipMergeBase, ') + ..write('zulipFeatureLevel: $zulipFeatureLevel, ') + ..write('ackedPushToken: $ackedPushToken') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, realmUrl, userId, email, apiKey, + zulipVersion, zulipMergeBase, zulipFeatureLevel, ackedPushToken); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AccountsData && + other.id == this.id && + other.realmUrl == this.realmUrl && + other.userId == this.userId && + other.email == this.email && + other.apiKey == this.apiKey && + other.zulipVersion == this.zulipVersion && + other.zulipMergeBase == this.zulipMergeBase && + other.zulipFeatureLevel == this.zulipFeatureLevel && + other.ackedPushToken == this.ackedPushToken); +} + +class AccountsCompanion extends UpdateCompanion { + final Value id; + final Value realmUrl; + final Value userId; + final Value email; + final Value apiKey; + final Value zulipVersion; + final Value zulipMergeBase; + final Value zulipFeatureLevel; + final Value ackedPushToken; + const AccountsCompanion({ + this.id = const Value.absent(), + this.realmUrl = const Value.absent(), + this.userId = const Value.absent(), + this.email = const Value.absent(), + this.apiKey = const Value.absent(), + this.zulipVersion = const Value.absent(), + this.zulipMergeBase = const Value.absent(), + this.zulipFeatureLevel = const Value.absent(), + this.ackedPushToken = const Value.absent(), + }); + AccountsCompanion.insert({ + this.id = const Value.absent(), + required String realmUrl, + required int userId, + required String email, + required String apiKey, + required String zulipVersion, + this.zulipMergeBase = const Value.absent(), + required int zulipFeatureLevel, + this.ackedPushToken = const Value.absent(), + }) : realmUrl = Value(realmUrl), + userId = Value(userId), + email = Value(email), + apiKey = Value(apiKey), + zulipVersion = Value(zulipVersion), + zulipFeatureLevel = Value(zulipFeatureLevel); + static Insertable custom({ + Expression? id, + Expression? realmUrl, + Expression? userId, + Expression? email, + Expression? apiKey, + Expression? zulipVersion, + Expression? zulipMergeBase, + Expression? zulipFeatureLevel, + Expression? ackedPushToken, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (realmUrl != null) 'realm_url': realmUrl, + if (userId != null) 'user_id': userId, + if (email != null) 'email': email, + if (apiKey != null) 'api_key': apiKey, + if (zulipVersion != null) 'zulip_version': zulipVersion, + if (zulipMergeBase != null) 'zulip_merge_base': zulipMergeBase, + if (zulipFeatureLevel != null) 'zulip_feature_level': zulipFeatureLevel, + if (ackedPushToken != null) 'acked_push_token': ackedPushToken, + }); + } + + AccountsCompanion copyWith( + {Value? id, + Value? realmUrl, + Value? userId, + Value? email, + Value? apiKey, + Value? zulipVersion, + Value? zulipMergeBase, + Value? zulipFeatureLevel, + Value? ackedPushToken}) { + return AccountsCompanion( + id: id ?? this.id, + realmUrl: realmUrl ?? this.realmUrl, + userId: userId ?? this.userId, + email: email ?? this.email, + apiKey: apiKey ?? this.apiKey, + zulipVersion: zulipVersion ?? this.zulipVersion, + zulipMergeBase: zulipMergeBase ?? this.zulipMergeBase, + zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ackedPushToken: ackedPushToken ?? this.ackedPushToken, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (realmUrl.present) { + map['realm_url'] = Variable(realmUrl.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (apiKey.present) { + map['api_key'] = Variable(apiKey.value); + } + if (zulipVersion.present) { + map['zulip_version'] = Variable(zulipVersion.value); + } + if (zulipMergeBase.present) { + map['zulip_merge_base'] = Variable(zulipMergeBase.value); + } + if (zulipFeatureLevel.present) { + map['zulip_feature_level'] = Variable(zulipFeatureLevel.value); + } + if (ackedPushToken.present) { + map['acked_push_token'] = Variable(ackedPushToken.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AccountsCompanion(') + ..write('id: $id, ') + ..write('realmUrl: $realmUrl, ') + ..write('userId: $userId, ') + ..write('email: $email, ') + ..write('apiKey: $apiKey, ') + ..write('zulipVersion: $zulipVersion, ') + ..write('zulipMergeBase: $zulipMergeBase, ') + ..write('zulipFeatureLevel: $zulipFeatureLevel, ') + ..write('ackedPushToken: $ackedPushToken') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV2 extends GeneratedDatabase { + DatabaseAtV2(QueryExecutor e) : super(e); + late final Accounts accounts = Accounts(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [accounts]; + @override + int get schemaVersion => 2; +}