Skip to content

Use Drift and SQLite for data storage #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 27, 2023
Merged
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ app.*.map.json
/android/app/profile
/android/app/release

# Scaffolding hack
# Old scaffolding hack
lib/credential_fixture.dart
33 changes: 0 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 21 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions lib/api/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ abstract class ApiConnection {
Future<String> postFileFromStream(String route, Stream<List<int>> content, int length, { String? filename });
}

// TODO memoize
Map<String, String> authHeader(Auth auth) {
final authBytes = utf8.encode("${auth.email}:${auth.apiKey}");
// TODO memoize auth header on LiveApiConnection and PerAccountStore
Map<String, String> authHeader({required String email, required String apiKey}) {
final authBytes = utf8.encode("$email:$apiKey");
return {
'Authorization': 'Basic ${base64.encode(authBytes)}',
};
Expand All @@ -65,7 +65,9 @@ class LiveApiConnection extends ApiConnection {
_isOpen = false;
}

Map<String, String> _headers() => authHeader(auth);
Map<String, String> _headers() {
return authHeader(email: auth.email, apiKey: auth.apiKey);
}

@override
Future<String> get(String route, Map<String, dynamic>? params) async {
Expand Down
97 changes: 97 additions & 0 deletions lib/model/database.dart
Original file line number Diff line number Diff line change
@@ -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<int> get id => integer().autoIncrement()();

Column<String> get realmUrl => text().map(const UriConverter())();
Column<int> get userId => integer()();

Column<String> get email => text()();
Column<String> get apiKey => text()();

Column<String> get zulipVersion => text()();
Column<String> get zulipMergeBase => text().nullable()();
Column<int> get zulipFeatureLevel => integer()();

Column<String> get ackedPushToken => text().nullable()();

@override
List<Set<Column<Object>>> get uniqueKeys => [
{realmUrl, userId},
{realmUrl, email},
];
Comment on lines +25 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add a TODO in lib/widgets/login.dart to handle exceptions thrown because of this, or maybe check GlobalStore.accountIds before sending a bad insert to the database.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought. I added one TODO(log) to log database errors (which we should do in a general way down in the Drift layer), and also a TODO(#35) in lib/widgets/login.dart to give the user feedback if the account is a duplicate.

}

class UriConverter extends TypeConverter<Uri, String> {
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<int> createAccount(AccountsCompanion values) {
return into(accounts).insert(values);
}
}
Loading