diff --git a/packages/powersync/README.md b/packages/powersync/README.md index 55b444df..611a54a8 100644 --- a/packages/powersync/README.md +++ b/packages/powersync/README.md @@ -11,48 +11,141 @@ * No need for client-side database migrations - these are handled automatically. * Subscribe to queries for live updates. +## Examples + +For complete app examples, see our [example app gallery](https://docs.powersync.com/resources/demo-apps-example-projects#flutter) + +For examples of some common patterns, see our [example snippets](./example/README.md) + ## Getting started +You'll need to create a PowerSync account and set up a PowerSync instance. You can do this at [https://www.powersync.com/](https://www.powersync.com/). + +### Install the package + +`flutter pub add powersync` + +### Implement a backend connector and initialize the PowerSync database + ```dart import 'package:powersync/powersync.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart'; +// Define the schema for the local SQLite database. +// You can automatically generate this schema based on your sync rules: +// In the PowerSync dashboard, right-click on your PowerSync instance and then click "Generate client-side schema" const schema = Schema([ Table('customers', [Column.text('name'), Column.text('email')]) ]); late PowerSyncDatabase db; -// Setup connector to backend if you would like to sync data. -class BackendConnector extends PowerSyncBackendConnector { +// You must implement a backend connector to define how PowerSync communicates with your backend. +class MyBackendConnector extends PowerSyncBackendConnector { PowerSyncDatabase db; - BackendConnector(this.db); + MyBackendConnector(this.db); @override Future fetchCredentials() async { - // implement fetchCredentials + // implement fetchCredentials to obtain a JWT from your authentication service + // see https://docs.powersync.com/usage/installation/authentication-setup } @override Future uploadData(PowerSyncDatabase database) async { - // implement uploadData + // Implement uploadData to send local changes to your backend service + // You can omit this method if you only want to sync data from the server to the client + // see https://docs.powersync.com/usage/installation/upload-data } } openDatabase() async { final dir = await getApplicationSupportDirectory(); final path = join(dir.path, 'powersync-dart.db'); + // Setup the database. db = PowerSyncDatabase(schema: schema, path: path); await db.initialize(); - // Run local statements. - await db.execute( + // Connect to backend + db.connect(connector: MyBackendConnector(db)); +} +``` + +### Subscribe to changes in data + +```dart +StreamBuilder( + // you can watch any SQL query + stream: return db.watch('SELECT * FROM customers order by id asc'), + builder: (context, snapshot) { + if (snapshot.hasData) { + // TODO: implement your own UI here based on the result set + return ...; + } else { + return const Center(child: CircularProgressIndicator()); + } + }, +) +``` + +### Insert, update, and delete data in the SQLite database as you would normally + +```dart +FloatingActionButton( + onPressed: () async { + await db.execute( 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', - ['Fred', 'fred@example.org']); + ['Fred', 'fred@example.org'], + ); + }, + tooltip: '+', + child: const Icon(Icons.add), +); +``` +### Send changes in local data to your backend service - // Connect to backend - db.connect(connector: BackendConnector(db)); +```dart +// Implement the uploadData method in your backend connector +@override +Future uploadData(PowerSyncDatabase database) async { + final batch = await database.getCrudBatch(); + if (batch == null) return; + for (var op in batch.crud) { + switch (op.op) { + case UpdateType.put: + // Send the data to your backend service + // replace `_myApi` with your own API client or service + await _myApi.put(op.table, op.opData!); + break; + default: + // TODO: implement the other operations (patch, delete) + break; + } + } + await batch.complete(); } ``` + +### Logging + +You can enable logging to see what's happening under the hood +or to debug connection/authentication/sync issues. + +```dart +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); + } + } +}); +``` + diff --git a/packages/powersync/lib/src/connector.dart b/packages/powersync/lib/src/connector.dart index 75b9e7a3..2d4ec38b 100644 --- a/packages/powersync/lib/src/connector.dart +++ b/packages/powersync/lib/src/connector.dart @@ -113,7 +113,8 @@ class PowerSyncCredentials { try { List parts = token.split('.'); if (parts.length == 3) { - final rawData = base64Decode(parts[1]); + // dart:convert doesn't like missing padding + final rawData = base64Url.decode(base64.normalize(parts[1])); final text = Utf8Decoder().convert(rawData); Map payload = jsonDecode(text); if (payload.containsKey('exp') && payload['exp'] is int) { diff --git a/packages/powersync/test/credentials_test.dart b/packages/powersync/test/credentials_test.dart new file mode 100644 index 00000000..fcdb2551 --- /dev/null +++ b/packages/powersync/test/credentials_test.dart @@ -0,0 +1,14 @@ +import 'package:powersync/powersync.dart'; +import 'package:test/test.dart'; + +void main() { + group('PowerSyncCredentials', () { + test('getExpiryDate', () async { + // Specifically test a token with a "-" character and missing padding + final token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ9Pn0-YWIiLCJpYXQiOjE3MDc3Mzk0MDAsImV4cCI6MTcwNzczOTUwMH0=.IVoAtpJ7jfwLbqlyJGYHPCvljLis_fHj2Qvdqlj8AQU'; + expect(PowerSyncCredentials.getExpiryDate(token)?.toUtc(), + equals(DateTime.parse('2024-02-12T12:05:00Z'))); + }); + }); +}