diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..7cd77fdcc5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# Suppress noisy generated files in diffs. + +# 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. +#pubspec.lock -diff 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/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/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/model/database.dart b/lib/model/database.dart new file mode 100644 index 0000000000..5bc257b7f5 --- /dev/null +++ b/lib/model/database.dart @@ -0,0 +1,97 @@ +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'; + +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()(); + + Column get ackedPushToken => text().nullable()(); + + @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()); + + // 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 => 2; // 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); + + if (from < 2 && 2 <= to) { + await m.addColumn(accounts, accounts.ackedPushToken); + } + // New migrations go here. + } + ); + } + + 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..613c5919ae --- /dev/null +++ b/lib/model/database.g.dart @@ -0,0 +1,472 @@ +// 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); + 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, + realmUrl, + userId, + email, + apiKey, + zulipVersion, + zulipMergeBase, + zulipFeatureLevel, + ackedPushToken + ]; + @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); + } + if (data.containsKey('acked_push_token')) { + context.handle( + _ackedPushTokenMeta, + ackedPushToken.isAcceptableOrUnknown( + data['acked_push_token']!, _ackedPushTokenMeta)); + } + 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'])!, + ackedPushToken: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}acked_push_token']), + ); + } + + @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; + final String? ackedPushToken; + 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, + this.ackedPushToken}); + @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); + 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 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']), + 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), + }; + } + + Account copyWith( + {int? id, + Uri? realmUrl, + int? userId, + String? email, + String? apiKey, + String? zulipVersion, + Value zulipMergeBase = const Value.absent(), + int? zulipFeatureLevel, + Value ackedPushToken = const Value.absent()}) => + 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, + ackedPushToken: + ackedPushToken.present ? ackedPushToken.value : this.ackedPushToken, + ); + @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('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 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 && + 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 Uri 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) { + 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); + } + 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(); + } +} + +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/lib/model/store.dart b/lib/model/store.dart index 394c2da670..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,9 +12,12 @@ 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'; +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 @@ -31,6 +38,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) @@ -105,20 +113,18 @@ abstract class GlobalStore extends ChangeNotifier { Account? getAccount(int id) => _accounts[id]; - // 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++; - 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(AccountsCompanion data); + // More mutators as needed: // Future updateAccount... } @@ -202,56 +208,69 @@ class PerAccountStore extends ChangeNotifier { } } -@immutable -class Account extends Auth { - Account({ - required super.realmUrl, - required super.email, - required super.apiKey, - required this.userId, - required this.zulipFeatureLevel, - required this.zulipVersion, - required this.zulipMergeBase, - }); - - final int userId; - final int zulipFeatureLevel; - final String zulipVersion; - final String? zulipMergeBase; -} - +/// 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); } -} -/// 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( - 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', -); + @override + Future doInsertAccount(AccountsCompanion data) async { + 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 [PerAccountStore] which polls an event queue to stay up to date. class LivePerAccountStore extends PerAccountStore { @@ -271,7 +290,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 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, diff --git a/lib/widgets/login.dart b/lib/widgets/login.dart index 5deddd6a02..f68e8398d5 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) { @@ -138,17 +135,17 @@ class _EmailPasswordLoginPageState extends State { return; } - final account = Account( + 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, 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/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 b5fe0a93c7..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: @@ -126,10 +142,18 @@ 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" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" clock: dependency: transitive description: @@ -214,10 +238,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: @@ -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: @@ -254,10 +294,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 +397,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: @@ -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 @@ -477,18 +517,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: @@ -561,22 +601,30 @@ 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: 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: @@ -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: @@ -802,10 +874,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 +890,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: 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/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/database_test.dart b/test/model/database_test.dart new file mode 100644 index 0000000000..ecd9b66859 --- /dev/null +++ b/test/model/database_test.dart @@ -0,0 +1,104 @@ +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() { + group('non-migration tests', () { + late AppDatabase database; + + 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, + }); + }); + }); + + 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, + }); + }); + }); +} + +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 + }; + } +} 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/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 new file mode 100644 index 0000000000..c8c6ff5926 --- /dev/null +++ b/test/model/schemas/schema.dart @@ -0,0 +1,21 @@ +// 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; +import 'schema_v2.dart' as v2; + +class GeneratedHelper implements SchemaInstantiationHelper { + @override + GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { + switch (version) { + case 1: + return v1.DatabaseAtV1(db); + case 2: + return v2.DatabaseAtV2(db); + default: + throw MissingSchemaException(version, const {1, 2}); + } + } +} 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; +} 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; +} diff --git a/test/model/store_test.dart b/test/model/store_test.dart index dd716ae583..409318f91d 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -109,4 +109,22 @@ class TestGlobalStore extends GlobalStore { (completers[account] ??= []).add(completer); return completer.future; } + + 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, + ); + } } 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 )