Skip to content

Commit 719103c

Browse files
committed
feat(database): implement collection seeding and indexing
- Add _seedCollection method to seed specific collections from fixture data - Implement indexing for various collections to enable efficient searches - Remove unused import of mongodb_rate_limit_service.dart - Refactor code to improve readability and maintainability
1 parent e5d6849 commit 719103c

File tree

1 file changed

+151
-105
lines changed

1 file changed

+151
-105
lines changed
Lines changed: 151 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'package:core/core.dart';
22
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';
43
import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart';
54
import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart';
65
import '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

Comments
 (0)