Skip to content

Commit fe51a1e

Browse files
committed
feat(db): replace postgres seeding service with mongodb
- Rewrites `DatabaseSeedingService` to work with a MongoDB `Db` instance. - Removes table creation logic, as MongoDB collections are schemaless. - Implements idempotent seeding using `bulkWrite` with `upsert` operations, preventing duplicate data on subsequent runs. - Correctly handles the conversion of string IDs from fixtures to MongoDB `ObjectId` for the `_id` field. - Ensures complex nested objects in fixtures are properly JSON-encoded before insertion.
1 parent c065be4 commit fe51a1e

File tree

1 file changed

+64
-96
lines changed

1 file changed

+64
-96
lines changed
Lines changed: 64 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,117 @@
1-
import 'dart:convert';
2-
import 'dart:io';
3-
41
import 'package:ht_shared/ht_shared.dart';
52
import 'package:logging/logging.dart';
63
import 'package:mongo_dart/mongo_dart.dart';
74

85
/// {@template database_seeding_service}
96
/// A service responsible for seeding the MongoDB database with initial data.
107
///
11-
/// This service reads data from local JSON fixture files and uses `upsert`
12-
/// operations to ensure that the seeding process is idempotent. It can be
13-
/// run multiple times without creating duplicate documents.
8+
/// This service reads data from predefined fixture lists in `ht_shared` and
9+
/// uses `upsert` operations to ensure that the seeding process is idempotent.
10+
/// It can be run multiple times without creating duplicate documents.
1411
/// {@endtemplate}
1512
class DatabaseSeedingService {
1613
/// {@macro database_seeding_service}
1714
const DatabaseSeedingService({required Db db, required Logger log})
18-
: _db = db,
19-
_log = log;
15+
: _db = db,
16+
_log = log;
2017

2118
final Db _db;
2219
final Logger _log;
2320

2421
/// The main entry point for seeding all necessary data.
2522
Future<void> seedInitialData() async {
2623
_log.info('Starting database seeding process...');
27-
await _seedCollection(
24+
25+
await _seedCollection<Country>(
2826
collectionName: 'countries',
29-
fixturePath: 'lib/src/fixtures/countries.json',
27+
fixtureData: countriesFixturesData,
28+
getId: (item) => item.id,
29+
toJson: (item) => item.toJson(),
3030
);
31-
await _seedCollection(
31+
await _seedCollection<Source>(
3232
collectionName: 'sources',
33-
fixturePath: 'lib/src/fixtures/sources.json',
33+
fixtureData: sourcesFixturesData,
34+
getId: (item) => item.id,
35+
toJson: (item) => item.toJson(),
3436
);
35-
await _seedCollection(
37+
await _seedCollection<Topic>(
3638
collectionName: 'topics',
37-
fixturePath: 'lib/src/fixtures/topics.json',
39+
fixtureData: topicsFixturesData,
40+
getId: (item) => item.id,
41+
toJson: (item) => item.toJson(),
3842
);
39-
await _seedCollection(
43+
await _seedCollection<Headline>(
4044
collectionName: 'headlines',
41-
fixturePath: 'lib/src/fixtures/headlines.json',
45+
fixtureData: headlinesFixturesData,
46+
getId: (item) => item.id,
47+
toJson: (item) => item.toJson(),
48+
);
49+
await _seedCollection<User>(
50+
collectionName: 'users',
51+
fixtureData: usersFixturesData,
52+
getId: (item) => item.id,
53+
toJson: (item) => item.toJson(),
54+
);
55+
await _seedCollection<RemoteConfig>(
56+
collectionName: 'remote_configs',
57+
fixtureData: remoteConfigsFixturesData,
58+
getId: (item) => item.id,
59+
toJson: (item) => item.toJson(),
4260
);
43-
await _seedInitialAdminAndConfig();
61+
4462
_log.info('Database seeding process completed.');
4563
}
4664

47-
/// Seeds a specific collection from a given JSON fixture file.
48-
Future<void> _seedCollection({
65+
/// Seeds a specific collection from a given list of fixture data.
66+
Future<void> _seedCollection<T>({
4967
required String collectionName,
50-
required String fixturePath,
68+
required List<T> fixtureData,
69+
required String Function(T) getId,
70+
required Map<String, dynamic> Function(T) toJson,
5171
}) async {
52-
_log.info('Seeding collection: "$collectionName" from "$fixturePath"...');
72+
_log.info('Seeding collection: "$collectionName"...');
5373
try {
54-
final collection = _db.collection(collectionName);
55-
final file = File(fixturePath);
56-
if (!await file.exists()) {
57-
_log.warning('Fixture file not found: $fixturePath. Skipping.');
58-
return;
59-
}
60-
61-
final content = await file.readAsString();
62-
final documents = jsonDecode(content) as List<dynamic>;
63-
64-
if (documents.isEmpty) {
74+
if (fixtureData.isEmpty) {
6575
_log.info('No documents to seed for "$collectionName".');
6676
return;
6777
}
6878

69-
final bulk = collection.initializeUnorderedBulkOperation();
79+
final collection = _db.collection(collectionName);
80+
final operations = <Map<String, Object>>[];
7081

71-
for (final doc in documents) {
72-
final docMap = doc as Map<String, dynamic>;
73-
final id = docMap['id'] as String?;
82+
for (final item in fixtureData) {
83+
final id = getId(item);
7484

75-
if (id == null || !ObjectId.isValidHexId(id)) {
76-
_log.warning('Skipping document with invalid or missing ID: $doc');
85+
if (!ObjectId.isValidHexId(id)) {
86+
_log.warning('Skipping document with invalid ID format: $id');
7787
continue;
7888
}
7989

8090
final objectId = ObjectId.fromHexString(id);
81-
// Remove the string 'id' field and use '_id' with ObjectId
82-
docMap.remove('id');
91+
final document = toJson(item)..remove('id');
92+
93+
operations.add({
94+
'replaceOne': {
95+
'filter': {'_id': objectId},
96+
'replacement': document,
97+
'upsert': true,
98+
},
99+
});
100+
}
83101

84-
bulk.find({'_id': objectId}).upsert().replaceOne(docMap);
102+
if (operations.isEmpty) {
103+
_log.info('No valid documents to write for "$collectionName".');
104+
return;
85105
}
86106

87-
final result = await bulk.execute();
107+
final result = await collection.bulkWrite(operations);
88108
_log.info(
89109
'Seeding for "$collectionName" complete. '
90110
'Upserted: ${result.nUpserted}, Modified: ${result.nModified}.',
91111
);
92112
} on Exception catch (e, s) {
93-
_log.severe(
94-
'Failed to seed collection "$collectionName" from "$fixturePath".',
95-
e,
96-
s,
97-
);
98-
// Re-throwing to halt the startup process if seeding fails.
99-
rethrow;
100-
}
101-
}
102-
103-
/// Seeds the initial admin user and remote config document.
104-
Future<void> _seedInitialAdminAndConfig() async {
105-
_log.info('Seeding initial admin user and remote config...');
106-
try {
107-
// --- Seed Admin User ---
108-
final usersCollection = _db.collection('users');
109-
final adminUser = User.fromJson(adminUserFixture);
110-
final adminDoc = adminUser.toJson()
111-
..['app_role'] = adminUser.appRole.name
112-
..['dashboard_role'] = adminUser.dashboardRole.name
113-
..['feed_action_status'] = jsonEncode(adminUser.feedActionStatus)
114-
..remove('id');
115-
116-
await usersCollection.updateOne(
117-
where.id(ObjectId.fromHexString(adminUser.id)),
118-
modify.set(
119-
'email',
120-
adminDoc['email'],
121-
).setAll(adminDoc), // Use setAll to add/update all fields
122-
upsert: true,
123-
);
124-
_log.info('Admin user seeded successfully.');
125-
126-
// --- Seed Remote Config ---
127-
final remoteConfigCollection = _db.collection('remote_config');
128-
final remoteConfig = RemoteConfig.fromJson(remoteConfigFixture);
129-
final remoteConfigDoc = remoteConfig.toJson()
130-
..['user_preference_limits'] =
131-
jsonEncode(remoteConfig.userPreferenceConfig.toJson())
132-
..['ad_config'] = jsonEncode(remoteConfig.adConfig.toJson())
133-
..['account_action_config'] =
134-
jsonEncode(remoteConfig.accountActionConfig.toJson())
135-
..['app_status'] = jsonEncode(remoteConfig.appStatus.toJson())
136-
..remove('id');
137-
138-
await remoteConfigCollection.updateOne(
139-
where.id(ObjectId.fromHexString(remoteConfig.id)),
140-
modify.setAll(remoteConfigDoc),
141-
upsert: true,
142-
);
143-
_log.info('Remote config seeded successfully.');
144-
} on Exception catch (e, s) {
145-
_log.severe('Failed to seed admin user or remote config.', e, s);
113+
_log.severe('Failed to seed collection "$collectionName".', e, s);
146114
rethrow;
147115
}
148116
}
149-
}
117+
}

0 commit comments

Comments
 (0)