11import 'package:core/core.dart' ;
22import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart' ;
3- import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_rate_limit_service.dart' ;
43import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart' ;
54import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart' ;
65import 'package:logging/logging.dart' ;
@@ -28,20 +27,156 @@ class DatabaseSeedingService {
2827
2928 await _ensureIndexes ();
3029 await _seedOverrideAdminUser ();
30+ await _seedCollection <Country >(
31+ collectionName: 'countries' ,
32+ fixtureData: countriesFixturesData,
33+ getId: (item) => item.id,
34+ toJson: (item) => item.toJson (),
35+ );
36+ await _seedCollection <Language >(
37+ collectionName: 'languages' ,
38+ fixtureData: languagesFixturesData,
39+ getId: (item) => item.id,
40+ toJson: (item) => item.toJson (),
41+ );
42+ await _seedCollection <RemoteConfig >(
43+ collectionName: 'remote_configs' ,
44+ fixtureData: remoteConfigsFixturesData,
45+ getId: (item) => item.id,
46+ toJson: (item) => item.toJson (),
47+ );
3148
3249 _log.info ('Database seeding process completed.' );
3350 }
3451
52+ /// Seeds a specific collection from a given list of fixture data.
53+ Future <void > _seedCollection <T >({
54+ required String collectionName,
55+ required List <T > fixtureData,
56+ required String Function (T ) getId,
57+ required Map <String , dynamic > Function (T ) toJson,
58+ }) async {
59+ _log.info ('Seeding collection: "$collectionName "...' );
60+ try {
61+ if (fixtureData.isEmpty) {
62+ _log.info ('No documents to seed for "$collectionName ".' );
63+ return ;
64+ }
65+
66+ final collection = _db.collection (collectionName);
67+ final operations = < Map <String , Object >> [];
68+
69+ for (final item in fixtureData) {
70+ // Use the predefined hex string ID from the fixture to create a
71+ // deterministic ObjectId. This is crucial for maintaining relationships
72+ // between documents (e.g., a headline and its source).
73+ final objectId = ObjectId .fromHexString (getId (item));
74+ final document = toJson (item)..remove ('id' );
75+
76+ operations.add ({
77+ // Use updateOne with $set to be less destructive than replaceOne.
78+ 'updateOne' : {
79+ // Filter by the specific, deterministic _id.
80+ 'filter' : {'_id' : objectId},
81+ // Set the fields of the document.
82+ 'update' : {r'$set' : document},
83+ 'upsert' : true ,
84+ },
85+ });
86+ }
87+
88+ final result = await collection.bulkWrite (operations);
89+ _log.info (
90+ 'Seeding for "$collectionName " complete. '
91+ 'Upserted: ${result .nUpserted }, Modified: ${result .nModified }.' ,
92+ );
93+ } on Exception catch (e, s) {
94+ _log.severe ('Failed to seed collection "$collectionName ".' , e, s);
95+ rethrow ;
96+ }
97+ }
98+
99+ /// Ensures that the necessary indexes exist on the collections.
100+ ///
101+ /// This method is idempotent; it will only create indexes if they do not
102+ /// already exist. It's crucial for enabling efficient text searches.
103+ Future <void > _ensureIndexes () async {
104+ _log.info ('Ensuring database indexes exist...' );
105+ try {
106+ // Text index for searching headlines by title
107+ await _db
108+ .collection ('headlines' )
109+ .createIndex (keys: {'title' : 'text' }, name: 'headlines_text_index' );
110+
111+ // Text index for searching topics by name
112+ await _db
113+ .collection ('topics' )
114+ .createIndex (keys: {'name' : 'text' }, name: 'topics_text_index' );
115+
116+ // Text index for searching sources by name
117+ await _db
118+ .collection ('sources' )
119+ .createIndex (keys: {'name' : 'text' }, name: 'sources_text_index' );
120+
121+ // --- TTL and Unique Indexes via runCommand ---
122+ // The following indexes are created using the generic `runCommand` because
123+ // they require specific options not exposed by the simpler `createIndex`
124+ // helper method in the `mongo_dart` library.
125+ // Specifically, `expireAfterSeconds` is needed for TTL indexes.
126+
127+ // Indexes for the verification codes collection
128+ await _db.runCommand ({
129+ 'createIndexes' : kVerificationCodesCollection,
130+ 'indexes' : [
131+ {
132+ // This is a TTL (Time-To-Live) index. MongoDB will automatically
133+ // delete documents from this collection when the `expiresAt` field's
134+ // value is older than the specified number of seconds (0).
135+ 'key' : {'expiresAt' : 1 },
136+ 'name' : 'expiresAt_ttl_index' ,
137+ 'expireAfterSeconds' : 0 ,
138+ },
139+ {
140+ // This ensures that each email can only have one pending
141+ // verification code at a time, preventing duplicates.
142+ 'key' : {'email' : 1 },
143+ 'name' : 'email_unique_index' ,
144+ 'unique' : true ,
145+ },
146+ ],
147+ });
148+
149+ // Index for the token blacklist collection
150+ await _db.runCommand ({
151+ 'createIndexes' : kBlacklistedTokensCollection,
152+ 'indexes' : [
153+ {
154+ // This is a TTL index. MongoDB will automatically delete documents
155+ // (blacklisted tokens) when the `expiry` field's value is past.
156+ 'key' : {'expiry' : 1 },
157+ 'name' : 'expiry_ttl_index' ,
158+ 'expireAfterSeconds' : 0 ,
159+ },
160+ ],
161+ });
162+
163+ _log.info ('Database indexes are set up correctly.' );
164+ } on Exception catch (e, s) {
165+ _log.severe ('Failed to create database indexes.' , e, s);
166+ // We rethrow here because if indexes can't be created,
167+ // critical features like search will fail.
168+ rethrow ;
169+ }
170+ }
171+
35172 /// Ensures the single administrator account is correctly configured based on
36173 /// the `OVERRIDE_ADMIN_EMAIL` environment variable.
37174 Future <void > _seedOverrideAdminUser () async {
38175 _log.info ('Checking for admin user override...' );
39176 final overrideEmail = EnvironmentConfig .overrideAdminEmail;
40177
41178 if (overrideEmail == null || overrideEmail.isEmpty) {
42- _log.info (
43- 'OVERRIDE_ADMIN_EMAIL not set. Skipping admin user override.' ,
44- );
179+ _log.info ('OVERRIDE_ADMIN_EMAIL not set. Skipping admin user override.' );
45180 return ;
46181 }
47182
@@ -89,9 +224,10 @@ class DatabaseSeedingService {
89224 ),
90225 );
91226
92- await usersCollection.insertOne (
93- {'_id' : newAdminId, ...newAdminUser.toJson ()..remove ('id' )},
94- );
227+ await usersCollection.insertOne ({
228+ '_id' : newAdminId,
229+ ...newAdminUser.toJson ()..remove ('id' ),
230+ });
95231
96232 // Create default settings and preferences for the new admin.
97233 await _createUserSubDocuments (newAdminId);
@@ -130,9 +266,10 @@ class DatabaseSeedingService {
130266 showPublishDateInHeadlineFeed: true ,
131267 ),
132268 );
133- await _db.collection ('user_app_settings' ).insertOne (
134- {'_id' : userId, ...defaultAppSettings.toJson ()..remove ('id' )},
135- );
269+ await _db.collection ('user_app_settings' ).insertOne ({
270+ '_id' : userId,
271+ ...defaultAppSettings.toJson ()..remove ('id' ),
272+ });
136273
137274 final defaultUserPreferences = UserContentPreferences (
138275 id: userId.oid,
@@ -141,100 +278,9 @@ class DatabaseSeedingService {
141278 followedTopics: const [],
142279 savedHeadlines: const [],
143280 );
144- await _db.collection ('user_content_preferences' ).insertOne (
145- {'_id' : userId, ...defaultUserPreferences.toJson ()..remove ('id' )},
146- );
147- }
148-
149- /// Ensures that the necessary indexes exist on the collections.
150- ///
151- /// This method is idempotent; it will only create indexes if they do not
152- /// already exist. It's crucial for enabling efficient text searches.
153- Future <void > _ensureIndexes () async {
154- _log.info ('Ensuring database indexes exist...' );
155- try {
156- // Text index for searching headlines by title
157- await _db
158- .collection ('headlines' )
159- .createIndex (keys: {'title' : 'text' }, name: 'headlines_text_index' );
160-
161- // Text index for searching topics by name
162- await _db
163- .collection ('topics' )
164- .createIndex (keys: {'name' : 'text' }, name: 'topics_text_index' );
165-
166- // Text index for searching sources by name
167- await _db
168- .collection ('sources' )
169- .createIndex (keys: {'name' : 'text' }, name: 'sources_text_index' );
170-
171- // --- TTL and Unique Indexes via runCommand ---
172- // The following indexes are created using the generic `runCommand` because
173- // they require specific options not exposed by the simpler `createIndex`
174- // helper method in the `mongo_dart` library.
175- // Specifically, `expireAfterSeconds` is needed for TTL indexes.
176-
177- // Indexes for the verification codes collection
178- await _db.runCommand ({
179- 'createIndexes' : kVerificationCodesCollection,
180- 'indexes' : [
181- {
182- // This is a TTL (Time-To-Live) index. MongoDB will automatically
183- // delete documents from this collection when the `expiresAt` field's
184- // value is older than the specified number of seconds (0).
185- 'key' : {'expiresAt' : 1 },
186- 'name' : 'expiresAt_ttl_index' ,
187- 'expireAfterSeconds' : 0 ,
188- },
189- {
190- // This ensures that each email can only have one pending
191- // verification code at a time, preventing duplicates.
192- 'key' : {'email' : 1 },
193- 'name' : 'email_unique_index' ,
194- 'unique' : true ,
195- },
196- ],
197- });
198-
199- // Index for the token blacklist collection
200- await _db.runCommand ({
201- 'createIndexes' : kBlacklistedTokensCollection,
202- 'indexes' : [
203- {
204- // This is a TTL index. MongoDB will automatically delete documents
205- // (blacklisted tokens) when the `expiry` field's value is past.
206- 'key' : {'expiry' : 1 },
207- 'name' : 'expiry_ttl_index' ,
208- 'expireAfterSeconds' : 0 ,
209- },
210- ],
211- });
212-
213- // Index for the rate limit attempts collection
214- await _db.runCommand ({
215- 'createIndexes' : kRateLimitAttemptsCollection,
216- 'indexes' : [
217- {
218- // This is a TTL index. MongoDB will automatically delete request
219- // attempt documents 24 hours after they are created.
220- 'key' : {'createdAt' : 1 },
221- 'name' : 'createdAt_ttl_index' ,
222- 'expireAfterSeconds' : 86400 , // 24 hours
223- },
224- {
225- // Index on the key field for faster lookups.
226- 'key' : {'key' : 1 },
227- 'name' : 'key_index' ,
228- },
229- ],
230- });
231-
232- _log.info ('Database indexes are set up correctly.' );
233- } on Exception catch (e, s) {
234- _log.severe ('Failed to create database indexes.' , e, s);
235- // We rethrow here because if indexes can't be created,
236- // critical features like search will fail.
237- rethrow ;
238- }
281+ await _db.collection ('user_content_preferences' ).insertOne ({
282+ '_id' : userId,
283+ ...defaultUserPreferences.toJson ()..remove ('id' ),
284+ });
239285 }
240286}
0 commit comments