Skip to content

Commit

Permalink
powersync prototype (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dieterbe committed Aug 2, 2024
1 parent 044cf6a commit fe48597
Show file tree
Hide file tree
Showing 12 changed files with 508 additions and 1 deletion.
60 changes: 60 additions & 0 deletions lib/api_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';

final log = Logger('powersync-test');

class ApiClient {
final String baseUrl;

ApiClient(this.baseUrl);

Future<Map<String, dynamic>> authenticate(String username, String password) async {
final response = await http.post(
Uri.parse('$baseUrl/api/auth/'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'username': username, 'password': password}),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to authenticate');
}
}

Future<Map<String, dynamic>> getToken(String userId) async {
final response = await http.get(
Uri.parse('$baseUrl/api/get_powersync_token/'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to fetch token');
}
}

Future<void> upsert(Map<String, dynamic> record) async {
await http.put(
Uri.parse('$baseUrl/api/upload_data/'),
headers: {'Content-Type': 'application/json'},
body: json.encode(record),
);
}

Future<void> update(Map<String, dynamic> record) async {
await http.patch(
Uri.parse('$baseUrl/api/upload_data/'),
headers: {'Content-Type': 'application/json'},
body: json.encode(record),
);
}

Future<void> delete(Map<String, dynamic> record) async {
await http.delete(
Uri.parse('$baseUrl/api/upload_data/'),
headers: {'Content-Type': 'application/json'},
body: json.encode(record),
);
}
}
4 changes: 4 additions & 0 deletions lib/app_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class AppConfig {
static const String djangoUrl = 'http://192.168.2.223:6061';
static const String powersyncUrl = 'http://192.168.2.223:8080';
}
21 changes: 21 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/locator.dart';
import 'package:wger/powersync.dart';
import 'package:wger/providers/add_exercise.dart';
import 'package:wger/providers/base_provider.dart';
import 'package:wger/providers/body_weight.dart';
Expand Down Expand Up @@ -52,15 +54,34 @@ import 'package:wger/screens/workout_plans_screen.dart';
import 'package:wger/theme/theme.dart';
import 'package:wger/widgets/core/about.dart';
import 'package:wger/widgets/core/settings.dart';
import 'package:logging/logging.dart';

import 'providers/auth.dart';

void main() async {
//zx.setLogEnabled(kDebugMode);
Logger.root.level = Level.INFO;
Logger.root.onRecord.listen((record) {
if (kDebugMode) {
print('[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}');

if (record.error != null) {
print(record.error);
}
if (record.stackTrace != null) {
print(record.stackTrace);
}
}
});

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

await openDatabase();

final loggedIn = await isLoggedIn();
print('is logged in $loggedIn');

// Locator to initialize exerciseDB
await ServiceLocator().configure();
// Application
Expand Down
54 changes: 54 additions & 0 deletions lib/models/schema.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:powersync/powersync.dart';

const todosTable = 'todos';

// these are the same ones as in postgres, except for 'id'
Schema schema = const Schema(([
Table(todosTable, [
Column.text('list_id'),
Column.text('created_at'),
Column.text('completed_at'),
Column.text('description'),
Column.integer('completed'),
Column.text('created_by'),
Column.text('completed_by'),
], indexes: [
// Index to allow efficient lookup within a list
Index('list', [IndexedColumn('list_id')])
]),
Table('lists',
[Column.text('created_at'), Column.text('name'), Column.text('owner_id')])
]));

// post gres columns:
// todos:
// id | created_at | completed_at | description | completed | created_by | completed_by | list_id
// lists:
// id | created_at | name | owner_id

// diagnostics app:
/*
new Schema([
new Table({
name: 'lists', // same as flutter
columns: [
new Column({ name: 'created_at', type: ColumnType.TEXT }),
new Column({ name: 'name', type: ColumnType.TEXT }),
new Column({ name: 'owner_id', type: ColumnType.TEXT })
]
}),
new Table({
name: 'todos', // misses completed_at and completed_by, until these actually get populated with something
columns: [
new Column({ name: 'created_at', type: ColumnType.TEXT }),
new Column({ name: 'description', type: ColumnType.TEXT }),
new Column({ name: 'completed', type: ColumnType.INTEGER }),
new Column({ name: 'created_by', type: ColumnType.TEXT }),
new Column({ name: 'list_id', type: ColumnType.TEXT })
]
})
])
Column.text('completed_at'),
Column.text('completed_by'),
*/
50 changes: 50 additions & 0 deletions lib/models/todo_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:wger/models/schema.dart';

import '../powersync.dart';
import 'package:powersync/sqlite3.dart' as sqlite;

/// TodoItem represents a result row of a query on "todos".
///
/// This class is immutable - methods on this class do not modify the instance
/// directly. Instead, watch or re-query the data to get the updated item.
/// confirm how the watch works. this seems like a weird pattern
class TodoItem {
final String id;
final String description;
final String? photoId;
final bool completed;

TodoItem(
{required this.id,
required this.description,
required this.completed,
required this.photoId});

factory TodoItem.fromRow(sqlite.Row row) {
return TodoItem(
id: row['id'],
description: row['description'],
photoId: row['photo_id'],
completed: row['completed'] == 1);
}

Future<void> toggle() async {
if (completed) {
await db.execute(
'UPDATE $todosTable SET completed = FALSE, completed_by = NULL, completed_at = NULL WHERE id = ?',
[id]);
} else {
await db.execute(
'UPDATE $todosTable SET completed = TRUE, completed_by = ?, completed_at = datetime() WHERE id = ?',
[await getUserId(), id]);
}
}

Future<void> delete() async {
await db.execute('DELETE FROM $todosTable WHERE id = ?', [id]);
}

static Future<void> addPhoto(String photoId, String id) async {
await db.execute('UPDATE $todosTable SET photo_id = ? WHERE id = ?', [photoId, id]);
}
}
96 changes: 96 additions & 0 deletions lib/models/todo_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import 'package:powersync/sqlite3.dart' as sqlite;

import 'todo_item.dart';
import '../powersync.dart';

/// TodoList represents a result row of a query on "lists".
///
/// This class is immutable - methods on this class do not modify the instance
/// directly. Instead, watch or re-query the data to get the updated list.
class TodoList {
/// List id (UUID).
final String id;

/// Descriptive name.
final String name;

/// Number of completed todos in this list.
final int? completedCount;

/// Number of pending todos in this list.
final int? pendingCount;

TodoList({required this.id, required this.name, this.completedCount, this.pendingCount});

factory TodoList.fromRow(sqlite.Row row) {
return TodoList(
id: row['id'],
name: row['name'],
completedCount: row['completed_count'],
pendingCount: row['pending_count']);
}

/// Watch all lists.
static Stream<List<TodoList>> watchLists() {
// This query is automatically re-run when data in "lists" or "todos" is modified.
return db.watch('SELECT * FROM lists ORDER BY created_at, id').map((results) {
return results.map(TodoList.fromRow).toList(growable: false);
});
}

/// Watch all lists, with [completedCount] and [pendingCount] populated.
static Stream<List<TodoList>> watchListsWithStats() {
// This query is automatically re-run when data in "lists" or "todos" is modified.
return db.watch('''
SELECT
*,
(SELECT count() FROM todos WHERE list_id = lists.id AND completed = TRUE) as completed_count,
(SELECT count() FROM todos WHERE list_id = lists.id AND completed = FALSE) as pending_count
FROM lists
ORDER BY created_at
''').map((results) {
return results.map(TodoList.fromRow).toList(growable: false);
});
}

/// Create a new list
static Future<TodoList> create(String name) async {
final results = await db.execute('''
INSERT INTO
lists(id, created_at, name, owner_id)
VALUES(uuid(), datetime(), ?, ?)
RETURNING *
''', [name, await getUserId()]);
return TodoList.fromRow(results.first);
}

/// Watch items within this list.
Stream<List<TodoItem>> watchItems() {
return db.watch('SELECT * FROM todos WHERE list_id = ? ORDER BY created_at DESC, id',
parameters: [id]).map((event) {
return event.map(TodoItem.fromRow).toList(growable: false);
});
}

/// Delete this list.
Future<void> delete() async {
await db.execute('DELETE FROM lists WHERE id = ?', [id]);
}

/// Find list item.
static Future<TodoList> find(id) async {
final results = await db.get('SELECT * FROM lists WHERE id = ?', [id]);
return TodoList.fromRow(results);
}

/// Add a new todo item to this list.
Future<TodoItem> add(String description) async {
final results = await db.execute('''
INSERT INTO
todos(id, created_at, completed, list_id, description, created_by)
VALUES(uuid(), datetime(), FALSE, ?, ?, ?)
RETURNING *
''', [id, description, await getUserId()]);
return TodoItem.fromRow(results.first);
}
}
Loading

0 comments on commit fe48597

Please sign in to comment.