Skip to content

Commit affa16d

Browse files
authored
Merge pull request #32 from flutter-news-app-full-source-code/feature-integrae-languade-model-into-teh-generic-data-route
Feature integrae languade model into teh generic data route
2 parents 11e03f9 + 719103c commit affa16d

File tree

6 files changed

+200
-105
lines changed

6 files changed

+200
-105
lines changed

lib/src/config/app_dependencies.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class AppDependencies {
5252
late final DataRepository<Topic> topicRepository;
5353
late final DataRepository<Source> sourceRepository;
5454
late final DataRepository<Country> countryRepository;
55+
late final DataRepository<Language> languageRepository;
5556
late final DataRepository<User> userRepository;
5657
late final DataRepository<UserAppSettings> userAppSettingsRepository;
5758
late final DataRepository<UserContentPreferences>
@@ -128,6 +129,13 @@ class AppDependencies {
128129
toJson: (item) => item.toJson(),
129130
logger: Logger('DataMongodb<Country>'),
130131
);
132+
final languageClient = DataMongodb<Language>(
133+
connectionManager: _mongoDbConnectionManager,
134+
modelName: 'languages',
135+
fromJson: Language.fromJson,
136+
toJson: (item) => item.toJson(),
137+
logger: Logger('DataMongodb<Language>'),
138+
);
131139
final userClient = DataMongodb<User>(
132140
connectionManager: _mongoDbConnectionManager,
133141
modelName: 'users',
@@ -162,6 +170,7 @@ class AppDependencies {
162170
topicRepository = DataRepository(dataClient: topicClient);
163171
sourceRepository = DataRepository(dataClient: sourceClient);
164172
countryRepository = DataRepository(dataClient: countryClient);
173+
languageRepository = DataRepository(dataClient: languageClient);
165174
userRepository = DataRepository(dataClient: userClient);
166175
userAppSettingsRepository = DataRepository(
167176
dataClient: userAppSettingsClient,

lib/src/rbac/permissions.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ abstract class Permissions {
2828
static const String countryUpdate = 'country.update';
2929
static const String countryDelete = 'country.delete';
3030

31+
// Language Permissions
32+
static const String languageCreate = 'language.create';
33+
static const String languageRead = 'language.read';
34+
static const String languageUpdate = 'language.update';
35+
static const String languageDelete = 'language.delete';
36+
3137
// User Permissions
3238
// Allows reading any user profile (e.g., for admin or public profiles)
3339
static const String userRead = 'user.read';

lib/src/rbac/role_permissions.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ final Set<String> _appGuestUserPermissions = {
88
Permissions.topicRead,
99
Permissions.sourceRead,
1010
Permissions.countryRead,
11+
Permissions.languageRead,
1112
Permissions.userAppSettingsReadOwned,
1213
Permissions.userAppSettingsUpdateOwned,
1314
Permissions.userContentPreferencesReadOwned,
@@ -30,9 +31,20 @@ final Set<String> _appPremiumUserPermissions = {
3031
// --- Dashboard Role Permissions ---
3132

3233
final Set<String> _dashboardPublisherPermissions = {
34+
// Publishers need to read all content types to manage them effectively.
35+
Permissions.headlineRead,
36+
Permissions.topicRead,
37+
Permissions.sourceRead,
38+
Permissions.countryRead,
39+
Permissions.languageRead,
40+
Permissions.remoteConfigRead,
41+
42+
// Publishers can manage headlines.
3343
Permissions.headlineCreate,
3444
Permissions.headlineUpdate,
3545
Permissions.headlineDelete,
46+
47+
// Core dashboard access and quality-of-life permissions.
3648
Permissions.dashboardLogin,
3749
Permissions.rateLimitingBypass,
3850
};
@@ -48,6 +60,9 @@ final Set<String> _dashboardAdminPermissions = {
4860
Permissions.countryCreate,
4961
Permissions.countryUpdate,
5062
Permissions.countryDelete,
63+
Permissions.languageCreate,
64+
Permissions.languageUpdate,
65+
Permissions.languageDelete,
5166
Permissions.userRead, // Allows reading any user's profile
5267
Permissions.remoteConfigCreate,
5368
Permissions.remoteConfigUpdate,
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
}

routes/_middleware.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ Handler middleware(Handler handler) {
9090
(_) => deps.countryRepository,
9191
),
9292
) //
93+
.use(
94+
provider<DataRepository<Language>>(
95+
(_) => deps.languageRepository,
96+
),
97+
) //
9398
.use(
9499
provider<DataRepository<User>>((_) => deps.userRepository),
95100
) //

0 commit comments

Comments
 (0)