From 93164afa7cb12808e575be505183259457f98ed9 Mon Sep 17 00:00:00 2001 From: Evan Ferrao Date: Tue, 30 Jul 2024 12:33:05 +0530 Subject: [PATCH] mynotes: Implement CRUD operations using sqlite --- lib/services/auth/auth_user.dart | 5 +- lib/services/crud/crud_crudexceptions.dart | 17 ++ lib/services/crud/notes_service.dart | 272 ++++++++++++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 148 ++++++++-- pubspec.yaml | 3 + 6 files changed, 421 insertions(+), 28 deletions(-) create mode 100644 lib/services/crud/crud_crudexceptions.dart create mode 100644 lib/services/crud/notes_service.dart diff --git a/lib/services/auth/auth_user.dart b/lib/services/auth/auth_user.dart index cbeb677..c645362 100644 --- a/lib/services/auth/auth_user.dart +++ b/lib/services/auth/auth_user.dart @@ -4,7 +4,8 @@ import 'package:flutter/material.dart'; @immutable class AuthUser { final bool isEmailVerified; - AuthUser({required this.isEmailVerified}); + const AuthUser({required this.isEmailVerified}); - factory AuthUser.fromFirebase(User user) => AuthUser(isEmailVerified: user.emailVerified); + factory AuthUser.fromFirebase(User user) => + AuthUser(isEmailVerified: user.emailVerified); } diff --git a/lib/services/crud/crud_crudexceptions.dart b/lib/services/crud/crud_crudexceptions.dart new file mode 100644 index 0000000..e11a252 --- /dev/null +++ b/lib/services/crud/crud_crudexceptions.dart @@ -0,0 +1,17 @@ +class DatabaseAlreadyOpenException implements Exception {} + +class UnableToGetDocumentsDirectory implements Exception {} + +class DatebaseNotOpen implements Exception {} + +class CouldNotDeleteUser implements Exception {} + +class UserAlreadyExists implements Exception {} + +class CouldNotFindUser implements Exception {} + +class CouldNotDeleteNote implements Exception {} + +class CouldNotFindNote implements Exception {} + +class CouldNotUpdateNote implements Exception {} diff --git a/lib/services/crud/notes_service.dart b/lib/services/crud/notes_service.dart new file mode 100644 index 0000000..4ad67bd --- /dev/null +++ b/lib/services/crud/notes_service.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; +import 'package:mynotes/services/crud/crud_crudexceptions.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart'; + +class NotesService { + Database? _db; + + Future updateNote({ + required DatabaseNote note, + required String text, + }) async { + final db = _getDatabaseOrThrow(); + await getNote(id: note.id); + final updatesCount = await db.update(noteTable, { + textColumn: text, + isSyncedWithCloudColumn: 0, + }); + + if (updatesCount == 0) { + throw CouldNotUpdateNote(); + } else { + return await getNote(id: note.id); + } + } + + Future> getAllNote({required int id}) async { + final db = _getDatabaseOrThrow(); + final notes = await db.query( + noteTable, + ); + return notes.map((noteRow) => DatabaseNote.fromRow(noteRow)); + // where is noteRow coming from? + // The noteRow variable is a parameter of the map method. The map method is called on the notes list, which is a list of rows returned by the query method. The map method takes a function as an argument and applies that function to each element of the list. In this case, the function is an anonymous function that takes a row as a parameter and returns a DatabaseNote object created from that row. + // what is the type of noteRow? + // The type of noteRow is Map. This is the type of the rows returned by the query method. Each row is a map where the keys are column names and the values are the corresponding values in the row. + // but noteRow is not defined in the function getAllNote? + // The noteRow variable is a parameter of the anonymous function passed to the map method. The map method applies the function to each element of the list and passes the element as an argument to the function. In this case, the element is a row returned by the query method, and the function creates a DatabaseNote object from that row. + } + + Future getNote({required int id}) async { + final db = _getDatabaseOrThrow(); + final notes = await db.query( + noteTable, + limit: 1, + where: '$idColumn = ?', + whereArgs: [id], + ); + if (notes.isEmpty) { + throw CouldNotFindNote(); + } else { + return DatabaseNote.fromRow(notes.first); + } + } + + Future deleteAllNotes() async { + final db = _getDatabaseOrThrow(); + return await db.delete(noteTable); + } + + Future deleteNote({required int id}) async { + final db = _getDatabaseOrThrow(); + final deletedCount = await db.delete( + noteTable, + where: '$idColumn = ?', + whereArgs: [id], + ); + if (deletedCount == 0) { + throw CouldNotDeleteNote(); + } + } + + Future createNote({required DatabaseUser owner}) async { + final db = _getDatabaseOrThrow(); + final dbUser = await getUser(email: owner.email); + + // make sure owner exists in the database with the correct id + if (dbUser != owner) { + throw CouldNotFindUser(); + } + + const text = ''; + // create the note + final noteId = await db.insert(noteTable, { + userIdColumn: owner.id, + textColumn: text, + isSyncedWithCloudColumn: 1, + }); + + final note = DatabaseNote( + id: noteId, + userId: owner.id, + text: text, + isSyncedWithCloud: true, + ); + return note; + } + + Future getUser({required String email}) async { + final db = _getDatabaseOrThrow(); + final user = await db.query( + userTable, + limit: 1, + where: '$emailColumn = ?', + whereArgs: [email.toLowerCase()], + ); + if (user.isEmpty) { + throw UserAlreadyExists(); + } else { + return DatabaseUser.fromRow(user.first); + } + } + + Future createUser({required String email}) async { + final db = _getDatabaseOrThrow(); + final results = await db.query( + userTable, + limit: 1, + where: '$emailColumn = ?', + whereArgs: [email.toLowerCase()], + ); + if (results.isNotEmpty) { + throw UserAlreadyExists(); + } + + final userId = await db.insert( + userTable, + {emailColumn: email.toLowerCase()}, + ); + return DatabaseUser(id: userId, email: email); + } + + Future deleteUser({required String email}) async { + final db = _getDatabaseOrThrow(); + final deletedCount = await db.delete( + userTable, + where: '$emailColumn = ?', + whereArgs: [email.toLowerCase()], + ); + if (deletedCount != 1) { + throw CouldNotDeleteUser(); + } + } + + Database _getDatabaseOrThrow() { + final db = _db; + if (db == null) { + throw DatebaseNotOpen(); + } else { + return db; + } + } + + Future close() async { + final db = _db; + if (db == null) { + throw DatebaseNotOpen(); + } else { + await db.close(); + _db = null; + } + } + + Future open() async { + if (_db != null) { + throw DatabaseAlreadyOpenException(); + } + try { + final docsPath = await getApplicationDocumentsDirectory(); + final dbPath = join(docsPath.path, dbName); + final db = await openDatabase(dbPath); + _db = db; + + // Create the user tables if they don't exist + await db.execute(createUserTable); + // Create the notes tables if they don't exist + await db.execute(createnoteTable); + } on MissingPlatformDirectoryException { + throw UnableToGetDocumentsDirectory(); + } + } +} + +@immutable +class DatabaseUser { + final int id; + final String email; + const DatabaseUser({ + required this.id, + required this.email, + }); + + DatabaseUser.fromRow(Map map) + // why Object? and not Object + // The Object? type allows the value to be null. If the value is null, the map[idColumn] as int expression will throw an exception. If the value is not null, the map[idColumn] as int expression will return the value as an int. + : id = map[idColumn] as int, + email = map[emailColumn] as String; + // where is the fromRow method? + // The fromRow method is a factory constructor that creates a DatabaseUser object from a row in the database. + // What is a factory constructor? + // A factory constructor is a constructor that returns an instance of a class. It is used to create objects of a class without exposing the constructor to the outside world. + // so can i call fromRow anything? or does it have to be fromRow? + // You can call the factory constructor anything you want. The name fromRow is a convention used to indicate that the method creates an object from a row in a database. + // can i name it fromMyRow or fromDatabaseRow? + // Yes, you can name it fromMyRow. The name of the factory constructor is up to you, but it is a good practice to use a name that describes what the method does. + + @override + String toString() => 'Person, ID = $id, email = $email'; + + @override + bool operator ==(covariant DatabaseUser other) => id == other.id; + + @override + int get hashCode => id.hashCode; +} + +class DatabaseNote { + final int id; + final int userId; + final String text; + final bool isSyncedWithCloud; + + const DatabaseNote({ + required this.id, + required this.userId, + required this.text, + required this.isSyncedWithCloud, + }); + + DatabaseNote.fromRow(Map map) + // what does fromRow do? + // The fromRow method is a factory constructor that creates a DatabaseNote object from a row in the database. + : id = map[idColumn] as int, + userId = map[userIdColumn] as int, + text = map[textColumn] as String, + isSyncedWithCloud = + (map[isSyncedWithCloudColumn] as int) == 1 ? true : false; + // isSyncedWithCloud = map[isSyncedWithCloudColumn] as bool; + // what is the difference between the two lines above? + // The first line converts the value of the isSyncedWithCloudColumn to a boolean value by checking if it is equal to 1. If it is equal to 1, it sets the value to true; otherwise, it sets it to false. + //The second line assumes that the value of the isSyncedWithCloudColumn is already a boolean value and assigns it directly to the isSyncedWithCloud property. + @override + String toString() => + 'Note, ID = $id, userId = $userId, isSyncedWithCloud = $isSyncedWithCloud, text = $text'; +} + +const dbName = 'notes.db'; +const noteTable = 'notes'; +const userTable = 'user'; +const idColumn = 'id'; +const emailColumn = 'email'; +const userIdColumn = 'user_id'; +const textColumn = 'text'; +const isSyncedWithCloudColumn = 'is_synced_with_cloud'; +const createUserTable = ''' + CREATE TABLE IF NOT EXISTS $userTable ( + $idColumn INTEGER NOT NULL, + $emailColumn TEXT NOT NULL UNIQUE, + PRIMARY KEY ($idColumn AUTOINCREMENT) + ); + '''; +const createnoteTable = ''' + CREATE TABLE IF NOT EXISTS $noteTable ( + $idColumn INTEGER NOT NULL, + $userIdColumn INTEGER NOT NULL, + $textColumn TEXT, + $isSyncedWithCloudColumn INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY ($userIdColumn) REFERENCES $userTable($idColumn), + PRIMARY KEY ($idColumn AUTOINCREMENT) + ); + '''; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 44ea85b..76ab4c5 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,10 +9,14 @@ import cloud_firestore import firebase_analytics import firebase_auth import firebase_core +import path_provider_foundation +import sqflite func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index ae21f86..be081c6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: a315d1c444402c3fa468de626d33a1c666041c87e9e195e8fb355b7084aefcc1 + sha256: b46f62516902afb04befa4b30eb6a12ac1f58ca8cb25fb9d632407259555dd3d url: "https://pub.dev" source: hosted - version: "1.3.38" + version: "1.3.39" analyzer: dependency: transitive description: @@ -69,26 +69,26 @@ packages: dependency: "direct main" description: name: cloud_firestore - sha256: "1232370ad04c21c699d0e73b2dc2e1c3b49258f89a16f0119036fd3c6e8aa2f5" + sha256: "240c1c3598e62ad58ee665b6df9c65172d2fbe4742770c21ed060e013cb8e037" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.1.0" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface - sha256: cfc64ae4a48bbb0ff6730b04f2d4653043c7a9b9008a991b1f2012a534b79e26 + sha256: "5b5a9c2b5a85bf995f12e7447c4197d7ad659533d642d3d904ccbb509f83d62a" url: "https://pub.dev" source: hosted - version: "6.2.8" + version: "6.2.9" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - sha256: "3db4e4c10feae18d80da86982781578c8e76cb6a43cf83110d8d6a62af9a952a" + sha256: "898e9f65548df65ca7b2cff9f32cf233424ceccff1d870b8819ea2b8050d5b39" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.3" collection: dependency: transitive description: @@ -137,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" file: dependency: transitive description: @@ -149,58 +157,58 @@ packages: dependency: "direct main" description: name: firebase_analytics - sha256: "726596f4ac3352978238274c33234435e61bdb811484ea3d6a2b857bf47a2715" + sha256: "2017da2cb0745fa912e13aadfc94e49691796cf6b7ed37edbca2a2388713da11" url: "https://pub.dev" source: hosted - version: "11.1.0" + version: "11.2.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: db445c727aa38038f91a3c6f6873d045a6740c79d03c0b6c61959e0c6ecfd771 + sha256: "4ea00fe31ff74c92c613e082193b55bd21f0653b536dd77cc1ba4cf60771d214" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.0" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "49a8a5ca0bf7fd7541e4b0915b8eb99816edc7a610d5078b89ae013a813fd567" + sha256: e5ce84f0c4e6fb8f8ca673d009445938a61845d8fa6b42c11913d5bbdbedf300 url: "https://pub.dev" source: hosted - version: "0.5.8" + version: "0.5.9" firebase_auth: dependency: "direct main" description: name: firebase_auth - sha256: "087fdcb54b0af6f4c5c756e1db4f90e9b65871b9b3a75fabaa0e0ee578301669" + sha256: a41b56878fa6aef3ea52962329b47eee333672d4b0ecc406e071b9fc729f242c url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.1.2" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "8fac689f71ac3489a785579e99a4bad24a93ad3d78c313fb786ee517012d25f1" + sha256: d1c68097588f3b75ef79a22102ff96c311735c254353bccf6824d19f1a7e86b9 url: "https://pub.dev" source: hosted - version: "7.4.1" + version: "7.4.2" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "486b2527fcfcab01278378d8d4791f4f7bee8a9f15bf35e801ba08fbdd84c234" + sha256: e66ec0ae5697ee39ccd4865d6887cb0df220dd4ea0b21404910c68ca4c1a731a url: "https://pub.dev" source: hosted - version: "5.12.3" + version: "5.12.4" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "1e06b0538ab3108a61d895ee16951670b491c4a94fce8f2d30e5de7a5eca4b28" + sha256: "5159984ce9b70727473eb388394650677c02c925aaa6c9439905e1f30966a4d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" firebase_core_platform_interface: dependency: transitive description: @@ -213,10 +221,10 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "6643fe3dbd021e6ccfb751f7882b39df355708afbdeb4130fc50f9305a9d1a3d" + sha256: "23509cb3cddfb3c910c143279ac3f07f06d3120f7d835e4a5d4b42558e978712" url: "https://pub.dev" source: hosted - version: "2.17.2" + version: "2.17.3" flutter: dependency: "direct main" description: flutter @@ -377,13 +385,69 @@ packages: source: hosted version: "2.1.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e" + url: "https://pub.dev" + source: hosted + version: "2.2.7" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -469,6 +533,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + url: "https://pub.dev" + source: hosted + version: "2.5.4" stack_trace: dependency: transitive description: @@ -493,6 +573,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -581,6 +669,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" yaml: dependency: transitive description: @@ -591,4 +687,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.1 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 405d0a2..2c6f7bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,9 @@ dependencies: firebase_auth: ^5.1.1 cloud_firestore: ^5.0.2 firebase_analytics: ^11.1.0 + sqflite: ^2.3.3+1 + path_provider: ^2.1.3 + path: ^1.9.0 dev_dependencies: flutter_test: