1
1
import 'package:core/core.dart' ;
2
2
import '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' ;
4
3
import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart' ;
5
4
import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart' ;
6
5
import 'package:logging/logging.dart' ;
@@ -28,20 +27,156 @@ class DatabaseSeedingService {
28
27
29
28
await _ensureIndexes ();
30
29
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
+ );
31
48
32
49
_log.info ('Database seeding process completed.' );
33
50
}
34
51
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
+
35
172
/// Ensures the single administrator account is correctly configured based on
36
173
/// the `OVERRIDE_ADMIN_EMAIL` environment variable.
37
174
Future <void > _seedOverrideAdminUser () async {
38
175
_log.info ('Checking for admin user override...' );
39
176
final overrideEmail = EnvironmentConfig .overrideAdminEmail;
40
177
41
178
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.' );
45
180
return ;
46
181
}
47
182
@@ -89,9 +224,10 @@ class DatabaseSeedingService {
89
224
),
90
225
);
91
226
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
+ });
95
231
96
232
// Create default settings and preferences for the new admin.
97
233
await _createUserSubDocuments (newAdminId);
@@ -130,9 +266,10 @@ class DatabaseSeedingService {
130
266
showPublishDateInHeadlineFeed: true ,
131
267
),
132
268
);
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
+ });
136
273
137
274
final defaultUserPreferences = UserContentPreferences (
138
275
id: userId.oid,
@@ -141,100 +278,9 @@ class DatabaseSeedingService {
141
278
followedTopics: const [],
142
279
savedHeadlines: const [],
143
280
);
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
+ });
239
285
}
240
286
}
0 commit comments