Skip to content

Commit 818bcf3

Browse files
committed
feat: db to json migration
1 parent eaa8891 commit 818bcf3

File tree

14 files changed

+562
-227
lines changed

14 files changed

+562
-227
lines changed

mobile-app/lib/app/app.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import 'package:freecodecamp/ui/views/profile/profile_view.dart';
3737
import 'package:freecodecamp/ui/views/settings/delete-account/delete_account_view.dart';
3838
import 'package:freecodecamp/ui/views/settings/settings_view.dart';
3939

40-
import 'package:sqflite_migration_service/sqflite_migration_service.dart';
4140
import 'package:stacked/stacked_annotations.dart';
4241
import 'package:stacked_services/stacked_services.dart';
4342

@@ -68,7 +67,6 @@ import 'package:stacked_services/stacked_services.dart';
6867
LazySingleton(classType: NavigationService),
6968
LazySingleton(classType: DialogService),
7069
LazySingleton(classType: SnackbarService),
71-
LazySingleton(classType: DatabaseMigrationService),
7270
LazySingleton(classType: PodcastsDatabaseService),
7371
LazySingleton(classType: NotificationService),
7472
LazySingleton(classType: DailyChallengeNotificationService),

mobile-app/lib/app/app.locator.dart

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
class BookmarkedTutorial {
2-
late int bookmarkId;
3-
late String tutorialTitle;
42
late String id;
5-
late String tutorialText;
3+
late String tutorialTitle;
64
late String authorName;
5+
late String tutorialText;
76

87
BookmarkedTutorial.fromMap(Map<String, dynamic> map) {
9-
bookmarkId = map['bookmark_id'];
10-
tutorialTitle = map['articleTitle'];
118
id = map['articleId'];
12-
tutorialText = map['articleText'];
9+
tutorialTitle = map['articleTitle'];
1310
authorName = map['authorName'];
11+
tutorialText = map['articleText'];
1412
}
1513

1614
BookmarkedTutorial({
17-
required this.bookmarkId,
18-
required this.tutorialTitle,
1915
required this.id,
20-
required this.tutorialText,
16+
required this.tutorialTitle,
2117
required this.authorName,
18+
required this.tutorialText,
2219
});
2320
}
Lines changed: 132 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,106 @@
1-
import 'dart:async';
21
import 'dart:developer';
32
import 'dart:io';
43

5-
import 'package:flutter/services.dart';
64
import 'package:freecodecamp/models/news/bookmarked_tutorial_model.dart';
75
import 'package:freecodecamp/models/news/tutorial_model.dart';
6+
import 'package:freecodecamp/service/news/legacy/bookmark_sqlite_migrator.dart';
7+
import 'package:freecodecamp/service/storage/json_file_store.dart';
88
import 'package:path/path.dart' as path;
9-
import 'package:sqflite/sqflite.dart';
10-
11-
const String bookmarksTableName = 'bookmarks';
9+
import 'package:path_provider/path_provider.dart';
1210

1311
class BookmarksDatabaseService {
14-
late Database _db;
15-
16-
Future initialise() async {
17-
String dbPath = await getDatabasesPath();
18-
String dbPathTutorials = path.join(dbPath, 'bookmarked-article.db');
19-
bool dbExists = await databaseExists(dbPathTutorials);
20-
21-
if (!dbExists) {
22-
// Making new copy from assets
23-
log('copying database from assets');
24-
try {
25-
await Directory(
26-
path.dirname(dbPathTutorials),
27-
).create(recursive: true);
28-
} catch (error) {
29-
log(error.toString());
12+
BookmarksDatabaseService({Directory? storageDirectoryOverride})
13+
: _storageDirectoryOverride = storageDirectoryOverride;
14+
15+
final Directory? _storageDirectoryOverride;
16+
final _legacyMigrator = const BookmarkSqliteMigrator();
17+
18+
JsonFileStore? _store;
19+
Future<void>? _initFuture;
20+
21+
Future<void> initialise() {
22+
_initFuture ??= _initialiseInternal();
23+
return _initFuture!;
24+
}
25+
26+
Future<void> _initialiseInternal() async {
27+
final baseDir =
28+
_storageDirectoryOverride ?? await getApplicationDocumentsDirectory();
29+
30+
final file = File(
31+
path.join(baseDir.path, 'storage', 'bookmarked-articles.json'),
32+
);
33+
final store = JsonFileStore(
34+
file: file,
35+
defaultValue: {
36+
'version': 1,
37+
'migratedFromSqlite': false,
38+
'bookmarks': [],
39+
},
40+
);
41+
await store.ensureExists();
42+
_store = store;
43+
44+
await _migrateFromSqliteIfNeeded();
45+
}
46+
47+
Future<void> _migrateFromSqliteIfNeeded() async {
48+
final store = _store;
49+
if (store == null) return;
50+
51+
await store.updateAndWrite((current) async {
52+
final migrated = current['migratedFromSqlite'] == true;
53+
final existing = current['bookmarks'] ?? [];
54+
if (migrated) return current;
55+
56+
// If the JSON store already has data, don't overwrite it.
57+
if (existing.isNotEmpty) {
58+
return {
59+
...current,
60+
'migratedFromSqlite': true,
61+
};
3062
}
3163

32-
ByteData data = await rootBundle.load(
33-
path.join(
34-
'assets',
35-
'database',
36-
'bookmarked-article.db',
37-
),
38-
);
39-
List<int> bytes = data.buffer.asUint8List(
40-
data.offsetInBytes,
41-
data.lengthInBytes,
42-
);
64+
final legacy = await _legacyMigrator.readBookmarks();
65+
if (legacy.isEmpty) {
66+
return {
67+
...current,
68+
'migratedFromSqlite': true,
69+
};
70+
}
4371

44-
await File(dbPathTutorials).writeAsBytes(bytes, flush: true);
45-
}
72+
final normalized = legacy.map((row) {
73+
return {
74+
'articleTitle': row['articleTitle'],
75+
'articleId': row['articleId'],
76+
'articleText': row['articleText'],
77+
'authorName': row['authorName'],
78+
};
79+
}).toList();
4680

47-
_db = await openDatabase(dbPathTutorials, version: 1);
81+
log('Migrated ${normalized.length} bookmarks from SQLite to JSON');
82+
return {
83+
...current,
84+
'migratedFromSqlite': true,
85+
'bookmarks': normalized,
86+
};
87+
});
4888
}
4989

5090
Map<String, dynamic> tutorialToMap(dynamic tutorial) {
5191
if (tutorial is Tutorial) {
5292
return {
53-
'articleTitle': tutorial.title,
5493
'articleId': tutorial.id,
94+
'articleTitle': tutorial.title,
95+
'authorName': tutorial.authorName,
5596
'articleText': tutorial.text,
56-
'authorName': tutorial.authorName
5797
};
5898
} else if (tutorial is BookmarkedTutorial) {
5999
return {
60-
'articleTitle': tutorial.tutorialTitle,
61100
'articleId': tutorial.id,
101+
'articleTitle': tutorial.tutorialTitle,
102+
'authorName': tutorial.authorName,
62103
'articleText': tutorial.tutorialText,
63-
'authorName': tutorial.authorName
64104
};
65105
} else {
66106
throw Exception(
@@ -70,50 +110,72 @@ class BookmarksDatabaseService {
70110
}
71111

72112
Future<List<BookmarkedTutorial>> getBookmarks() async {
73-
List<Map<String, dynamic>> bookmarksResults =
74-
await _db.query(bookmarksTableName);
113+
await initialise();
114+
final store = _store!;
115+
116+
final data = await store.read();
117+
final raw = (data['bookmarks'] as List?) ?? <dynamic>[];
75118

76-
List bookmarks = bookmarksResults
77-
.map(
78-
(tutorial) => BookmarkedTutorial.fromMap(tutorial),
79-
)
119+
final normalized = <Map<String, dynamic>>[];
120+
for (var i = 0; i < raw.length; i++) {
121+
final entry = raw[i];
122+
if (entry is Map) {
123+
normalized.add(Map<String, dynamic>.from(entry));
124+
}
125+
}
126+
127+
final bookmarks = normalized
128+
.map((tutorial) => BookmarkedTutorial.fromMap(tutorial))
80129
.toList();
81130

82-
return List.from(bookmarks.reversed);
131+
return List<BookmarkedTutorial>.from(bookmarks.reversed);
83132
}
84133

85134
Future<bool> isBookmarked(dynamic tutorial) async {
86-
List<Map<String, dynamic>> bookmarksResults = await _db.query(
87-
bookmarksTableName,
88-
where: 'articleId = ?',
89-
whereArgs: [tutorial.id],
90-
);
91-
return bookmarksResults.isNotEmpty;
135+
await initialise();
136+
final store = _store!;
137+
final data = await store.read();
138+
final raw = (data['bookmarks'] as List?) ?? <dynamic>[];
139+
return raw.any((e) => e is Map && e['articleId'] == tutorial.id);
92140
}
93141

94142
Future addBookmark(dynamic tutorial) async {
95-
try {
96-
await _db.insert(
97-
bookmarksTableName,
98-
tutorialToMap(tutorial),
99-
conflictAlgorithm: ConflictAlgorithm.replace,
100-
);
143+
await initialise();
144+
final store = _store!;
145+
await store.updateAndWrite((current) async {
146+
final raw = (current['bookmarks'] as List?) ?? <dynamic>[];
147+
final list = raw
148+
.whereType<Map>()
149+
.map((e) => Map<String, dynamic>.from(e))
150+
.toList();
151+
152+
list.removeWhere((e) => e['articleId'] == tutorial.id);
153+
list.add(tutorialToMap(tutorial));
154+
101155
log('Added bookmark: ${tutorial.id}');
102-
} catch (e) {
103-
log('Could not insert the bookmark: $e');
104-
}
156+
return {
157+
...current,
158+
'bookmarks': list,
159+
};
160+
});
105161
}
106162

107163
Future removeBookmark(dynamic tutorial) async {
108-
try {
109-
await _db.delete(
110-
bookmarksTableName,
111-
where: 'articleId = ?',
112-
whereArgs: [tutorial.id],
113-
);
164+
await initialise();
165+
final store = _store!;
166+
await store.updateAndWrite((current) async {
167+
final raw = (current['bookmarks'] as List?) ?? <dynamic>[];
168+
final list = raw
169+
.whereType<Map>()
170+
.map((e) => Map<String, dynamic>.from(e))
171+
.toList();
172+
173+
list.removeWhere((e) => e['articleId'] == tutorial.id);
114174
log('Removed bookmark: ${tutorial.id}');
115-
} catch (e) {
116-
log('Could not remove the bookmark: $e');
117-
}
175+
return {
176+
...current,
177+
'bookmarks': list,
178+
};
179+
});
118180
}
119181
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'dart:developer';
2+
3+
import 'package:path/path.dart' as path;
4+
import 'package:sqflite/sqflite.dart';
5+
6+
const String _bookmarksTableName = 'bookmarks';
7+
8+
class BookmarkSqliteMigrator {
9+
const BookmarkSqliteMigrator();
10+
11+
Future<List<Map<String, dynamic>>> readBookmarks() async {
12+
String dbFilePath;
13+
try {
14+
final dbPath = await getDatabasesPath();
15+
dbFilePath = path.join(dbPath, 'bookmarked-article.db');
16+
17+
final exists = await databaseExists(dbFilePath);
18+
if (!exists) return [];
19+
} catch (e) {
20+
// In some environments (e.g., unit tests without sqflite factory init),
21+
// any sqflite call can throw. Treat that as "no legacy DB".
22+
log('BookmarkSqliteMigrator unavailable: $e');
23+
return [];
24+
}
25+
26+
Database? db;
27+
try {
28+
db = await openDatabase(dbFilePath, version: 1);
29+
final results = await db.query(_bookmarksTableName);
30+
return results;
31+
} catch (e) {
32+
log('BookmarkSqliteMigrator failed: $e');
33+
return [];
34+
} finally {
35+
try {
36+
await db?.close();
37+
} catch (_) {}
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)