Skip to content

Commit fe48597

Browse files
committed
powersync prototype (WIP)
1 parent 044cf6a commit fe48597

12 files changed

+508
-1
lines changed

lib/api_client.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import 'dart:convert';
2+
import 'package:http/http.dart' as http;
3+
import 'package:logging/logging.dart';
4+
5+
final log = Logger('powersync-test');
6+
7+
class ApiClient {
8+
final String baseUrl;
9+
10+
ApiClient(this.baseUrl);
11+
12+
Future<Map<String, dynamic>> authenticate(String username, String password) async {
13+
final response = await http.post(
14+
Uri.parse('$baseUrl/api/auth/'),
15+
headers: {'Content-Type': 'application/json'},
16+
body: json.encode({'username': username, 'password': password}),
17+
);
18+
if (response.statusCode == 200) {
19+
return json.decode(response.body);
20+
} else {
21+
throw Exception('Failed to authenticate');
22+
}
23+
}
24+
25+
Future<Map<String, dynamic>> getToken(String userId) async {
26+
final response = await http.get(
27+
Uri.parse('$baseUrl/api/get_powersync_token/'),
28+
headers: {'Content-Type': 'application/json'},
29+
);
30+
if (response.statusCode == 200) {
31+
return json.decode(response.body);
32+
} else {
33+
throw Exception('Failed to fetch token');
34+
}
35+
}
36+
37+
Future<void> upsert(Map<String, dynamic> record) async {
38+
await http.put(
39+
Uri.parse('$baseUrl/api/upload_data/'),
40+
headers: {'Content-Type': 'application/json'},
41+
body: json.encode(record),
42+
);
43+
}
44+
45+
Future<void> update(Map<String, dynamic> record) async {
46+
await http.patch(
47+
Uri.parse('$baseUrl/api/upload_data/'),
48+
headers: {'Content-Type': 'application/json'},
49+
body: json.encode(record),
50+
);
51+
}
52+
53+
Future<void> delete(Map<String, dynamic> record) async {
54+
await http.delete(
55+
Uri.parse('$baseUrl/api/upload_data/'),
56+
headers: {'Content-Type': 'application/json'},
57+
body: json.encode(record),
58+
);
59+
}
60+
}

lib/app_config.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class AppConfig {
2+
static const String djangoUrl = 'http://192.168.2.223:6061';
3+
static const String powersyncUrl = 'http://192.168.2.223:8080';
4+
}

lib/main.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
*/
1818

19+
import 'package:flutter/foundation.dart';
1920
import 'package:flutter/material.dart';
2021
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2122
import 'package:provider/provider.dart';
2223
import 'package:wger/core/locator.dart';
24+
import 'package:wger/powersync.dart';
2325
import 'package:wger/providers/add_exercise.dart';
2426
import 'package:wger/providers/base_provider.dart';
2527
import 'package:wger/providers/body_weight.dart';
@@ -52,15 +54,34 @@ import 'package:wger/screens/workout_plans_screen.dart';
5254
import 'package:wger/theme/theme.dart';
5355
import 'package:wger/widgets/core/about.dart';
5456
import 'package:wger/widgets/core/settings.dart';
57+
import 'package:logging/logging.dart';
5558

5659
import 'providers/auth.dart';
5760

5861
void main() async {
5962
//zx.setLogEnabled(kDebugMode);
63+
Logger.root.level = Level.INFO;
64+
Logger.root.onRecord.listen((record) {
65+
if (kDebugMode) {
66+
print('[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}');
67+
68+
if (record.error != null) {
69+
print(record.error);
70+
}
71+
if (record.stackTrace != null) {
72+
print(record.stackTrace);
73+
}
74+
}
75+
});
6076

6177
// Needs to be called before runApp
6278
WidgetsFlutterBinding.ensureInitialized();
6379

80+
await openDatabase();
81+
82+
final loggedIn = await isLoggedIn();
83+
print('is logged in $loggedIn');
84+
6485
// Locator to initialize exerciseDB
6586
await ServiceLocator().configure();
6687
// Application

lib/models/schema.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:powersync/powersync.dart';
2+
3+
const todosTable = 'todos';
4+
5+
// these are the same ones as in postgres, except for 'id'
6+
Schema schema = const Schema(([
7+
Table(todosTable, [
8+
Column.text('list_id'),
9+
Column.text('created_at'),
10+
Column.text('completed_at'),
11+
Column.text('description'),
12+
Column.integer('completed'),
13+
Column.text('created_by'),
14+
Column.text('completed_by'),
15+
], indexes: [
16+
// Index to allow efficient lookup within a list
17+
Index('list', [IndexedColumn('list_id')])
18+
]),
19+
Table('lists',
20+
[Column.text('created_at'), Column.text('name'), Column.text('owner_id')])
21+
]));
22+
23+
// post gres columns:
24+
// todos:
25+
// id | created_at | completed_at | description | completed | created_by | completed_by | list_id
26+
// lists:
27+
// id | created_at | name | owner_id
28+
29+
// diagnostics app:
30+
/*
31+
new Schema([
32+
new Table({
33+
name: 'lists', // same as flutter
34+
columns: [
35+
new Column({ name: 'created_at', type: ColumnType.TEXT }),
36+
new Column({ name: 'name', type: ColumnType.TEXT }),
37+
new Column({ name: 'owner_id', type: ColumnType.TEXT })
38+
]
39+
}),
40+
new Table({
41+
name: 'todos', // misses completed_at and completed_by, until these actually get populated with something
42+
columns: [
43+
new Column({ name: 'created_at', type: ColumnType.TEXT }),
44+
new Column({ name: 'description', type: ColumnType.TEXT }),
45+
new Column({ name: 'completed', type: ColumnType.INTEGER }),
46+
new Column({ name: 'created_by', type: ColumnType.TEXT }),
47+
new Column({ name: 'list_id', type: ColumnType.TEXT })
48+
]
49+
})
50+
])
51+
52+
Column.text('completed_at'),
53+
Column.text('completed_by'),
54+
*/

lib/models/todo_item.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import 'package:wger/models/schema.dart';
2+
3+
import '../powersync.dart';
4+
import 'package:powersync/sqlite3.dart' as sqlite;
5+
6+
/// TodoItem represents a result row of a query on "todos".
7+
///
8+
/// This class is immutable - methods on this class do not modify the instance
9+
/// directly. Instead, watch or re-query the data to get the updated item.
10+
/// confirm how the watch works. this seems like a weird pattern
11+
class TodoItem {
12+
final String id;
13+
final String description;
14+
final String? photoId;
15+
final bool completed;
16+
17+
TodoItem(
18+
{required this.id,
19+
required this.description,
20+
required this.completed,
21+
required this.photoId});
22+
23+
factory TodoItem.fromRow(sqlite.Row row) {
24+
return TodoItem(
25+
id: row['id'],
26+
description: row['description'],
27+
photoId: row['photo_id'],
28+
completed: row['completed'] == 1);
29+
}
30+
31+
Future<void> toggle() async {
32+
if (completed) {
33+
await db.execute(
34+
'UPDATE $todosTable SET completed = FALSE, completed_by = NULL, completed_at = NULL WHERE id = ?',
35+
[id]);
36+
} else {
37+
await db.execute(
38+
'UPDATE $todosTable SET completed = TRUE, completed_by = ?, completed_at = datetime() WHERE id = ?',
39+
[await getUserId(), id]);
40+
}
41+
}
42+
43+
Future<void> delete() async {
44+
await db.execute('DELETE FROM $todosTable WHERE id = ?', [id]);
45+
}
46+
47+
static Future<void> addPhoto(String photoId, String id) async {
48+
await db.execute('UPDATE $todosTable SET photo_id = ? WHERE id = ?', [photoId, id]);
49+
}
50+
}

lib/models/todo_list.dart

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'package:powersync/sqlite3.dart' as sqlite;
2+
3+
import 'todo_item.dart';
4+
import '../powersync.dart';
5+
6+
/// TodoList represents a result row of a query on "lists".
7+
///
8+
/// This class is immutable - methods on this class do not modify the instance
9+
/// directly. Instead, watch or re-query the data to get the updated list.
10+
class TodoList {
11+
/// List id (UUID).
12+
final String id;
13+
14+
/// Descriptive name.
15+
final String name;
16+
17+
/// Number of completed todos in this list.
18+
final int? completedCount;
19+
20+
/// Number of pending todos in this list.
21+
final int? pendingCount;
22+
23+
TodoList({required this.id, required this.name, this.completedCount, this.pendingCount});
24+
25+
factory TodoList.fromRow(sqlite.Row row) {
26+
return TodoList(
27+
id: row['id'],
28+
name: row['name'],
29+
completedCount: row['completed_count'],
30+
pendingCount: row['pending_count']);
31+
}
32+
33+
/// Watch all lists.
34+
static Stream<List<TodoList>> watchLists() {
35+
// This query is automatically re-run when data in "lists" or "todos" is modified.
36+
return db.watch('SELECT * FROM lists ORDER BY created_at, id').map((results) {
37+
return results.map(TodoList.fromRow).toList(growable: false);
38+
});
39+
}
40+
41+
/// Watch all lists, with [completedCount] and [pendingCount] populated.
42+
static Stream<List<TodoList>> watchListsWithStats() {
43+
// This query is automatically re-run when data in "lists" or "todos" is modified.
44+
return db.watch('''
45+
SELECT
46+
*,
47+
(SELECT count() FROM todos WHERE list_id = lists.id AND completed = TRUE) as completed_count,
48+
(SELECT count() FROM todos WHERE list_id = lists.id AND completed = FALSE) as pending_count
49+
FROM lists
50+
ORDER BY created_at
51+
''').map((results) {
52+
return results.map(TodoList.fromRow).toList(growable: false);
53+
});
54+
}
55+
56+
/// Create a new list
57+
static Future<TodoList> create(String name) async {
58+
final results = await db.execute('''
59+
INSERT INTO
60+
lists(id, created_at, name, owner_id)
61+
VALUES(uuid(), datetime(), ?, ?)
62+
RETURNING *
63+
''', [name, await getUserId()]);
64+
return TodoList.fromRow(results.first);
65+
}
66+
67+
/// Watch items within this list.
68+
Stream<List<TodoItem>> watchItems() {
69+
return db.watch('SELECT * FROM todos WHERE list_id = ? ORDER BY created_at DESC, id',
70+
parameters: [id]).map((event) {
71+
return event.map(TodoItem.fromRow).toList(growable: false);
72+
});
73+
}
74+
75+
/// Delete this list.
76+
Future<void> delete() async {
77+
await db.execute('DELETE FROM lists WHERE id = ?', [id]);
78+
}
79+
80+
/// Find list item.
81+
static Future<TodoList> find(id) async {
82+
final results = await db.get('SELECT * FROM lists WHERE id = ?', [id]);
83+
return TodoList.fromRow(results);
84+
}
85+
86+
/// Add a new todo item to this list.
87+
Future<TodoItem> add(String description) async {
88+
final results = await db.execute('''
89+
INSERT INTO
90+
todos(id, created_at, completed, list_id, description, created_by)
91+
VALUES(uuid(), datetime(), FALSE, ?, ?, ?)
92+
RETURNING *
93+
''', [id, description, await getUserId()]);
94+
return TodoItem.fromRow(results.first);
95+
}
96+
}

0 commit comments

Comments
 (0)