Module Ecosystem and Extensions
Popular Community Modules
The Hypermodern ecosystem includes a rich collection of community-contributed modules that extend the platform's capabilities. These modules provide pre-built solutions for common use cases and integrate seamlessly with the core platform.
Authentication Modules
OAuth2 Provider Module
// hypermodern_oauth2/lib/oauth2_module.dart
class OAuth2Module extends HypermodernModule {
@override
ModuleManifest get manifest => ModuleManifest.fromJson({
'name': 'hypermodern_oauth2',
'version': '2.1.0',
'description': 'OAuth2 authentication provider with multiple grant types',
'author': 'Hypermodern Community',
'dependencies': {
'hypermodern_auth': '^1.0.0',
'hypermodern_crypto': '^1.0.0',
},
'exports': {
'models': ['OAuthClient', 'AccessToken', 'RefreshToken', 'AuthorizationCode'],
'endpoints': ['authorize', 'token', 'revoke', 'introspect'],
'services': ['OAuth2Service', 'ClientService', 'TokenService'],
},
'configuration': {
'authorization_code_ttl': {
'type': 'duration',
'default': '10m',
'description': 'Authorization code time-to-live'
},
'access_token_ttl': {
'type': 'duration',
'default': '1h',
'description': 'Access token time-to-live'
},
'refresh_token_ttl': {
'type': 'duration',
'default': '30d',
'description': 'Refresh token time-to-live'
},
'supported_grant_types': {
'type': 'array',
'default': ['authorization_code', 'client_credentials', 'refresh_token'],
'description': 'Supported OAuth2 grant types'
}
}
});
@override
Future<void> register(ModuleContainer container) async {
// Register services
container.singleton<OAuth2Service>((c) => OAuth2Service(
database: c.get<Database>(),
config: c.config,
cryptoService: c.get<CryptographyService>(),
));
container.singleton<ClientService>((c) => ClientService(
database: c.get<Database>(),
));
container.singleton<TokenService>((c) => TokenService(
database: c.get<Database>(),
config: c.config,
));
}
@override
Future<void> boot(ModuleContainer container) async {
final server = container.get<HypermodernServer>();
final oauth2Service = container.get<OAuth2Service>();
// Authorization endpoint
server.registerEndpoint<AuthorizeRequest, AuthorizeResponse>(
'authorize',
(request) async => await oauth2Service.authorize(request),
);
// Token endpoint
server.registerEndpoint<TokenRequest, TokenResponse>(
'token',
(request) async => await oauth2Service.token(request),
);
// Token revocation
server.registerEndpoint<RevokeRequest, RevokeResponse>(
'revoke',
(request) async => await oauth2Service.revoke(request),
);
// Token introspection
server.registerEndpoint<IntrospectRequest, IntrospectResponse>(
'introspect',
(request) async => await oauth2Service.introspect(request),
);
}
@override
List<Migration> get migrations => [
CreateOAuthClientsTableMigration(),
CreateAccessTokensTableMigration(),
CreateRefreshTokensTableMigration(),
CreateAuthorizationCodesTableMigration(),
];
}
class OAuth2Service {
final Database _db;
final ModuleConfiguration _config;
final CryptographyService _crypto;
OAuth2Service({
required Database database,
required ModuleConfiguration config,
required CryptographyService cryptoService,
}) : _db = database,
_config = config,
_crypto = cryptoService;
Future<AuthorizeResponse> authorize(AuthorizeRequest request) async {
// Validate client
final client = await _validateClient(request.clientId);
// Validate redirect URI
if (!client.redirectUris.contains(request.redirectUri)) {
throw OAuth2Exception('invalid_request', 'Invalid redirect URI');
}
// Validate response type
if (request.responseType != 'code') {
throw OAuth2Exception('unsupported_response_type', 'Only authorization code flow supported');
}
// Generate authorization code
final code = await _generateAuthorizationCode(
clientId: request.clientId,
userId: request.userId,
scope: request.scope,
redirectUri: request.redirectUri,
);
return AuthorizeResponse(
code: code,
state: request.state,
);
}
Future<TokenResponse> token(TokenRequest request) async {
switch (request.grantType) {
case 'authorization_code':
return await _handleAuthorizationCodeGrant(request);
case 'client_credentials':
return await _handleClientCredentialsGrant(request);
case 'refresh_token':
return await _handleRefreshTokenGrant(request);
default:
throw OAuth2Exception('unsupported_grant_type', 'Grant type not supported');
}
}
Future<TokenResponse> _handleAuthorizationCodeGrant(TokenRequest request) async {
// Validate authorization code
final codeData = await _validateAuthorizationCode(request.code!);
// Validate client
await _validateClient(request.clientId!, request.clientSecret);
// Validate redirect URI
if (request.redirectUri != codeData.redirectUri) {
throw OAuth2Exception('invalid_grant', 'Redirect URI mismatch');
}
// Generate tokens
final accessToken = await _generateAccessToken(
clientId: request.clientId!,
userId: codeData.userId,
scope: codeData.scope,
);
final refreshToken = await _generateRefreshToken(
clientId: request.clientId!,
userId: codeData.userId,
scope: codeData.scope,
);
// Revoke authorization code
await _revokeAuthorizationCode(request.code!);
return TokenResponse(
accessToken: accessToken.token,
tokenType: 'Bearer',
expiresIn: _config.getDuration('access_token_ttl').inSeconds,
refreshToken: refreshToken.token,
scope: codeData.scope,
);
}
Future<String> _generateAuthorizationCode({
required String clientId,
required int userId,
required String scope,
required String redirectUri,
}) async {
final code = _crypto.generateSecureToken(length: 32);
final expiresAt = DateTime.now().add(_config.getDuration('authorization_code_ttl'));
await _db.query('''
INSERT INTO authorization_codes (code, client_id, user_id, scope, redirect_uri, expires_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', [code, clientId, userId, scope, redirectUri, expiresAt, DateTime.now()]);
return code;
}
Future<AccessToken> _generateAccessToken({
required String clientId,
required int userId,
required String scope,
}) async {
final token = _crypto.generateSecureToken(length: 64);
final expiresAt = DateTime.now().add(_config.getDuration('access_token_ttl'));
await _db.query('''
INSERT INTO access_tokens (token, client_id, user_id, scope, expires_at, created_at)
VALUES (?, ?, ?, ?, ?, ?)
''', [token, clientId, userId, scope, expiresAt, DateTime.now()]);
return AccessToken(
token: token,
clientId: clientId,
userId: userId,
scope: scope,
expiresAt: expiresAt,
);
}
}
SAML Authentication Module
class SAMLModule extends HypermodernModule {
@override
ModuleManifest get manifest => ModuleManifest.fromJson({
'name': 'hypermodern_saml',
'version': '1.0.0',
'description': 'SAML 2.0 authentication integration',
'dependencies': {
'hypermodern_auth': '^1.0.0',
'hypermodern_crypto': '^1.0.0',
},
'configuration': {
'identity_providers': {
'type': 'array',
'required': true,
'description': 'List of SAML identity providers'
},
'service_provider': {
'type': 'object',
'required': true,
'properties': {
'entity_id': {'type': 'string', 'required': true},
'assertion_consumer_service_url': {'type': 'string', 'required': true},
'certificate': {'type': 'string', 'required': true},
'private_key': {'type': 'string', 'required': true}
}
}
}
});
@override
Future<void> boot(ModuleContainer container) async {
final server = container.get<HypermodernServer>();
final samlService = container.get<SAMLService>();
// SAML SSO initiation
server.registerEndpoint<SAMLLoginRequest, SAMLLoginResponse>(
'saml_login',
(request) async => await samlService.initiateLogin(request),
);
// SAML assertion consumer service
server.registerEndpoint<SAMLAssertionRequest, SAMLAssertionResponse>(
'saml_acs',
(request) async => await samlService.consumeAssertion(request),
);
// SAML metadata endpoint
server.registerEndpoint<SAMLMetadataRequest, SAMLMetadataResponse>(
'saml_metadata',
(request) async => await samlService.getMetadata(request),
);
}
}
Database Integration Modules
MongoDB Module
class MongoDBModule extends HypermodernModule {
@override
ModuleManifest get manifest => ModuleManifest.fromJson({
'name': 'hypermodern_mongodb',
'version': '1.5.0',
'description': 'MongoDB integration with ODM capabilities',
'configuration': {
'connection_string': {
'type': 'string',
'required': true,
'description': 'MongoDB connection string'
},
'database_name': {
'type': 'string',
'required': true,
'description': 'Default database name'
},
'connection_pool_size': {
'type': 'int32',
'default': 10,
'description': 'Maximum connection pool size'
}
}
});
@override
Future<void> register(ModuleContainer container) async {
// Register MongoDB client
container.singleton<MongoClient>((c) => MongoClient(
connectionString: c.config.getString('connection_string'),
poolSize: c.config.getInt('connection_pool_size'),
));
// Register database
container.singleton<MongoDatabase>((c) {
final client = c.get<MongoClient>();
return client.database(c.config.getString('database_name'));
});
// Register ODM
container.singleton<MongoODM>((c) => MongoODM(
database: c.get<MongoDatabase>(),
));
}
}
class MongoODM {
final MongoDatabase _database;
final Map<Type, MongoCollection> _collections = {};
MongoODM({required MongoDatabase database}) : _database = database;
MongoRepository<T> repository<T>() {
final collection = _getCollection<T>();
return MongoRepository<T>(collection);
}
MongoCollection _getCollection<T>() {
return _collections.putIfAbsent(T, () {
final collectionName = _getCollectionName<T>();
return _database.collection(collectionName);
});
}
String _getCollectionName<T>() {
// Convert class name to snake_case collection name
final className = T.toString();
return className.replaceAllMapped(
RegExp(r'[A-Z]'),
(match) => '_${match.group(0)!.toLowerCase()}',
).substring(1);
}
}
class MongoRepository<T> {
final MongoCollection _collection;
MongoRepository(this._collection);
Future<T?> findById(String id) async {
final doc = await _collection.findOne({'_id': ObjectId.fromHexString(id)});
return doc != null ? _fromDocument<T>(doc) : null;
}
Future<List<T>> find(Map<String, dynamic> filter) async {
final cursor = _collection.find(filter);
final docs = await cursor.toList();
return docs.map((doc) => _fromDocument<T>(doc)).toList();
}
Future<T> save(T entity) async {
final doc = _toDocument(entity);
if (doc.containsKey('_id')) {
await _collection.replaceOne({'_id': doc['_id']}, doc);
} else {
final result = await _collection.insertOne(doc);
doc['_id'] = result.insertedId;
}
return _fromDocument<T>(doc);
}
Future<void> delete(String id) async {
await _collection.deleteOne({'_id': ObjectId.fromHexString(id)});
}
T _fromDocument<T>(Map<String, dynamic> doc) {
// Convert MongoDB document to Dart object
// This would use reflection or code generation
return (T as dynamic).fromJson(doc);
}
Map<String, dynamic> _toDocument(T entity) {
// Convert Dart object to MongoDB document
return (entity as dynamic).toJson();
}
}
Redis Module
class RedisModule extends HypermodernModule {
@override
ModuleManifest get manifest => ModuleManifest.fromJson({
'name': 'hypermodern_redis',
'version': '2.0.0',
'description': 'Redis integration for caching and pub/sub',
'configuration': {
'connection_string': {
'type': 'string',
'default': 'redis://localhost:6379',
'description': 'Redis connection string'
},
'key_prefix': {
'type': 'string',
'default': 'hypermodern:',
'description': 'Key prefix for all Redis operations'
}
}
});
@override
Future<void> register(ModuleContainer container) async {
container.singleton<RedisClient>((c) => RedisClient(
connectionString: c.config.getString('connection_string'),
keyPrefix: c.config.getString('key_prefix'),
));
container.singleton<RedisCache>((c) => RedisCache(
client: c.get<RedisClient>(),
));
container.singleton<RedisPubSub>((c) => RedisPubSub(
client: c.get<RedisClient>(),
));
}
}
class RedisCache implements CacheProvider {
final RedisClient _client;
RedisCache({required RedisClient client}) : _client = client;
@override
Future<T?> get<T>(String key, T Function(String) deserializer) async {
final value = await _client.get(key);
return value != null ? deserializer(value) : null;
}
@override
Future<void> set<T>(String key, T value, {Duration? ttl}) async {
final serialized = jsonEncode((value as dynamic).toJson());
if (ttl != null) {
await _client.setex(key, ttl.inSeconds, serialized);
} else {
await _client.set(key, serialized);
}
}
@override
Future<void> delete(String key) async {
await _client.del([key]);
}
@override
Future<void> clear() async {
final keys = await _client.keys('*');
if (keys.isNotEmpty) {
await _client.del(keys);
}
}
}
class RedisPubSub {
final RedisClient _client;
final Map<String, StreamController<String>> _subscriptions = {};
RedisPubSub({required RedisClient client}) : _client = client;
Future<void> publish(String channel, String message) async {
await _client.publish(channel, message);
}
Stream<String> subscribe(String channel) {
if (_subscriptions.containsKey(channel)) {
return _subscriptions[channel]!.stream;
}
final controller = StreamController<String>.broadcast();
_subscriptions[channel] = controller;
_client.subscribe([channel]).listen((message) {
if (message.channel == channel) {
controller.add(message.payload);
}
});
return controller.stream;
}
Future<void> unsubscribe(String channel) async {
final controller = _subscriptions.remove(channel);
if (controller != null) {
await controller.close();
await _client.unsubscribe([channel]);
}
}
}
Third-Party Service Integration Modules
Stripe Payment Module
class StripeModule extends HypermodernModule {
@override
ModuleManifest get manifest => ModuleManifest.fromJson({
'name': 'hypermodern_stripe',
'version': '3.0.0',
'description': 'Stripe payment processing integration',
'configuration': {
'secret_key': {
'type': 'string',
'required': true,
'description': 'Stripe secret key'
},
'webhook_secret': {
'type': 'string',
'required': true,
'description': 'Stripe webhook endpoint secret'
},
'default_currency': {
'type': 'string',
'default': 'usd',
'description': 'Default currency for payments'
}
}
});
@override
Future<void> register(ModuleContainer container) async {
container.singleton<StripeClient>((c) => StripeClient(
secretKey: c.config.getString('secret_key'),
));
container.singleton<PaymentService>((c) => PaymentService(
stripeClient: c.get<StripeClient>(),
config: c.config,
));
container.singleton<WebhookService>((c) => WebhookService(
stripeClient: c.get<StripeClient>(),
webhookSecret: c.config.getString('webhook_secret'),
));
}
@override
Future<void> boot(ModuleContainer container) async {
final server = container.get<HypermodernServer>();
final paymentService = container.get<PaymentService>();
final webhookService = container.get<WebhookService>();
// Payment endpoints
server.registerEndpoint<CreatePaymentIntentRequest, PaymentIntentResponse>(
'create_payment_intent',
(request) async => await paymentService.createPaymentIntent(request),
);
server.registerEndpoint<ConfirmPaymentRequest, PaymentResponse>(
'confirm_payment',
(request) async => await paymentService.confirmPayment(request),
);
// Webhook endpoint
server.registerEndpoint<StripeWebhookRequest, WebhookResponse>(
'stripe_webhook',
(request) async => await webhookService.handleWebhook(request),
);
}
}
class PaymentService {
final StripeClient _stripe;
final ModuleConfiguration _config;
PaymentService({
required StripeClient stripeClient,
required ModuleConfiguration config,
}) : _stripe = stripeClient,
_config = config;
Future<PaymentIntentResponse> createPaymentIntent(CreatePaymentIntentRequest request) async {
final paymentIntent = await _stripe.paymentIntents.create({
'amount': request.amount,
'currency': request.currency ?? _config.getString('default_currency'),
'customer': request.customerId,
'metadata': request.metadata,
'automatic_payment_methods': {'enabled': true},
});
return PaymentIntentResponse(
id: paymentIntent.id,
clientSecret: paymentIntent.clientSecret,
status: paymentIntent.status,
amount: paymentIntent.amount,
currency: paymentIntent.currency,
);
}
Future<PaymentResponse> confirmPayment(ConfirmPaymentRequest request) async {
final paymentIntent = await _stripe.paymentIntents.confirm(
request.paymentIntentId,
{
'payment_method': request.paymentMethodId,
'return_url': request.returnUrl,
},
);
return PaymentResponse(
id: paymentIntent.id,
status: paymentIntent.status,
amount: paymentIntent.amount,
currency: paymentIntent.currency,
nextAction: paymentIntent.nextAction,
);
}
}
SendGrid Email Module
class SendGridModule extends HypermodernModule {
@override
ModuleManifest get manifest => ModuleManifest.fromJson({
'name': 'hypermodern_sendgrid',
'version': '1.2.0',
'description': 'SendGrid email service integration',
'configuration': {
'api_key': {
'type': 'string',
'required': true,
'description': 'SendGrid API key'
},
'default_from_email': {
'type': 'string',
'required': true,
'description': 'Default sender email address'
},
'default_from_name': {
'type': 'string',
'default': 'Hypermodern App',
'description': 'Default sender name'
}
}
});
@override
Future<void> register(ModuleContainer container) async {
container.singleton<SendGridClient>((c) => SendGridClient(
apiKey: c.config.getString('api_key'),
));
container.singleton<EmailService>((c) => EmailService(
sendGridClient: c.get<SendGridClient>(),
config: c.config,
));
container.singleton<TemplateService>((c) => TemplateService(
sendGridClient: c.get<SendGridClient>(),
));
}
@override
Future<void> boot(ModuleContainer container) async {
final server = container.get<HypermodernServer>();
final emailService = container.get<EmailService>();
server.registerEndpoint<SendEmailRequest, SendEmailResponse>(
'send_email',
(request) async => await emailService.sendEmail(request),
);
server.registerEndpoint<SendTemplateEmailRequest, SendEmailResponse>(
'send_template_email',
(request) async => await emailService.sendTemplateEmail(request),
);
}
}
class EmailService {
final SendGridClient _sendGrid;
final ModuleConfiguration _config;
EmailService({
required SendGridClient sendGridClient,
required ModuleConfiguration config,
}) : _sendGrid = sendGridClient,
_config = config;
Future<SendEmailResponse> sendEmail(SendEmailRequest request) async {
final email = Email(
from: EmailAddress(
email: request.fromEmail ?? _config.getString('default_from_email'),
name: request.fromName ?? _config.getString('default_from_name'),
),
to: request.to.map((addr) => EmailAddress(email: addr)).toList(),
subject: request.subject,
content: [
if (request.textContent != null)
Content(type: 'text/plain', value: request.textContent!),
if (request.htmlContent != null)
Content(type: 'text/html', value: request.htmlContent!),
],
attachments: request.attachments?.map((att) => Attachment(
content: att.content,
filename: att.filename,
type: att.contentType,
)).toList(),
);
final response = await _sendGrid.send(email);
return SendEmailResponse(
messageId: response.messageId,
status: response.statusCode == 202 ? 'sent' : 'failed',
);
}
Future<SendEmailResponse> sendTemplateEmail(SendTemplateEmailRequest request) async {
final email = TemplateEmail(
from: EmailAddress(
email: _config.getString('default_from_email'),
name: _config.getString('default_from_name'),
),
to: request.to.map((addr) => EmailAddress(email: addr)).toList(),
templateId: request.templateId,
dynamicTemplateData: request.templateData,
);
final response = await _sendGrid.sendTemplate(email);
return SendEmailResponse(
messageId: response.messageId,
status: response.statusCode == 202 ? 'sent' : 'failed',
);
}
}
Creating Module Libraries
Module Development Best Practices
abstract class ModuleDevelopmentGuide {
// 1. Module Structure
static const String moduleStructure = '''
my_module/
├── lib/
│ ├── src/
│ │ ├── models/
│ │ ├── services/
│ │ ├── providers/
│ │ └── middleware/
│ ├── my_module.dart # Main export file
│ └── module.dart # Module definition
├── schemas/
│ ├── models.json
│ ├── endpoints.json
│ └── enums.json
├── test/
│ ├── unit/
│ ├── integration/
│ └── module_test.dart
├── example/
│ └── main.dart
├── CHANGELOG.md
├── LICENSE
├── README.md
├── module.json # Module manifest
└── pubspec.yaml
''';
// 2. Naming Conventions
static const Map<String, String> namingConventions = {
'module_name': 'hypermodern_feature_name (snake_case)',
'class_names': 'PascalCase',
'method_names': 'camelCase',
'constants': 'SCREAMING_SNAKE_CASE',
'files': 'snake_case.dart',
};
// 3. Documentation Requirements
static const List<String> documentationRequirements = [
'Comprehensive README with usage examples',
'API documentation for all public methods',
'Configuration options documentation',
'Migration guide for breaking changes',
'Contributing guidelines',
];
// 4. Testing Requirements
static const List<String> testingRequirements = [
'Unit tests for all services and utilities',
'Integration tests for module functionality',
'Example application demonstrating usage',
'Performance benchmarks for critical paths',
'Compatibility tests with different Hypermodern versions',
];
}
// Example module template
class ExampleModule extends HypermodernModule {
@override
ModuleManifest get manifest => ModuleManifest.fromJson({
'name': 'hypermodern_example',
'version': '1.0.0',
'description': 'Example module demonstrating best practices',
'author': 'Your Name <your.email@example.com>',
'homepage': 'https://github.com/yourorg/hypermodern_example',
'repository': 'https://github.com/yourorg/hypermodern_example.git',
'license': 'MIT',
// Dependencies
'hypermodern_version': '>=1.0.0 <2.0.0',
'dependencies': {
'hypermodern_auth': '^1.0.0',
},
'dev_dependencies': {
'hypermodern_testing': '^1.0.0',
},
// Exports
'exports': {
'models': ['ExampleModel'],
'endpoints': ['example_endpoint'],
'services': ['ExampleService'],
'middleware': ['ExampleMiddleware'],
},
// Configuration schema
'configuration': {
'feature_enabled': {
'type': 'bool',
'default': true,
'description': 'Enable the example feature',
},
'api_endpoint': {
'type': 'string',
'required': true,
'description': 'External API endpoint URL',
},
'timeout': {
'type': 'duration',
'default': '30s',
'description': 'Request timeout duration',
},
},
// Permissions required by this module
'permissions': [
'example.read',
'example.write',
],
// Database tables created by this module
'database_tables': [
'example_data',
'example_logs',
],
// Module metadata
'tags': ['example', 'template', 'best-practices'],
'keywords': ['hypermodern', 'module', 'example'],
});
@override
Future<void> register(ModuleContainer container) async {
// Register services with dependency injection
container.singleton<ExampleService>((c) => ExampleService(
database: c.get<Database>(),
config: c.config,
httpClient: c.get<HttpClient>(),
));
// Register middleware
container.singleton<ExampleMiddleware>((c) => ExampleMiddleware(
service: c.get<ExampleService>(),
));
// Register event listeners
container.singleton<ExampleEventListener>((c) => ExampleEventListener(
service: c.get<ExampleService>(),
));
}
@override
Future<void> boot(ModuleContainer container) async {
final server = container.get<HypermodernServer>();
final service = container.get<ExampleService>();
final middleware = container.get<ExampleMiddleware>();
// Register endpoints
server.registerEndpoint<ExampleRequest, ExampleResponse>(
'example_endpoint',
(request) async => await service.handleRequest(request),
);
// Register middleware conditionally
if (container.config.getBool('feature_enabled')) {
server.middleware.add(middleware);
}
// Set up event listeners
final eventListener = container.get<ExampleEventListener>();
server.events.listen(eventListener.handleEvent);
}
@override
List<Migration> get migrations => [
CreateExampleDataTableMigration(),
CreateExampleLogsTableMigration(),
AddExampleIndexesMigration(),
];
@override
Future<void> onInstall(ModuleContext context) async {
// Perform installation tasks
await _createDefaultConfiguration(context);
await _setupExternalIntegrations(context);
print('✅ Example module installed successfully');
}
@override
Future<void> onUninstall(ModuleContext context) async {
// Cleanup tasks
await _cleanupExternalIntegrations(context);
await _archiveData(context);
print('🗑️ Example module uninstalled');
}
@override
Future<void> onUpdate(ModuleContext context, String fromVersion) async {
// Handle version-specific updates
if (_shouldMigrateFrom(fromVersion, '0.9.0')) {
await _migrateFromV0_9_0(context);
}
print('🔄 Example module updated from $fromVersion to ${manifest.version}');
}
Future<void> _createDefaultConfiguration(ModuleContext context) async {
// Create default configuration files
final configPath = path.join(context.modulePath, 'config', 'default.yaml');
await File(configPath).parent.create(recursive: true);
await File(configPath).writeAsString('''
example:
feature_enabled: true
timeout: 30s
log_level: info
''');
}
Future<void> _setupExternalIntegrations(ModuleContext context) async {
// Set up external service integrations
final apiEndpoint = context.configuration.getString('api_endpoint');
// Validate external service connectivity
final client = HttpClient();
try {
final response = await client.get('$apiEndpoint/health');
if (response.statusCode != 200) {
throw ModuleInstallationException('External service not available');
}
} catch (e) {
throw ModuleInstallationException('Failed to connect to external service: $e');
}
}
bool _shouldMigrateFrom(String fromVersion, String targetVersion) {
// Version comparison logic
return Version.parse(fromVersion) < Version.parse(targetVersion);
}
}
Module Testing Framework
class ModuleTestFramework {
final ModuleContainer _container;
final TestDatabase _testDb;
final MockHttpServer _mockServer;
ModuleTestFramework({
required ModuleContainer container,
required TestDatabase testDb,
required MockHttpServer mockServer,
}) : _container = container,
_testDb = testDb,
_mockServer = mockServer;
Future<void> testModule<T extends HypermodernModule>(T module) async {
group('${module.manifest.name} Module Tests', () {
setUpAll(() async {
await _testDb.initialize();
await _mockServer.start();
await module.register(_container);
await module.boot(_container);
});
tearDownAll(() async {
await _testDb.cleanup();
await _mockServer.stop();
});
group('Module Lifecycle', () {
test('should register services correctly', () async {
// Test service registration
for (final service in module.manifest.exports['services'] ?? []) {
expect(_container.has(service), isTrue, reason: 'Service $service not registered');
}
});
test('should boot without errors', () async {
// Module should boot successfully
expect(() => module.boot(_container), returnsNormally);
});
test('should handle installation', () async {
final context = MockModuleContext();
await module.onInstall(context);
// Verify installation side effects
expect(context.installationCompleted, isTrue);
});
});
group('Configuration', () {
test('should validate configuration schema', () {
final config = module.manifest.configuration;
for (final entry in config.entries) {
final field = entry.value;
// Test required fields
if (field['required'] == true) {
expect(
() => _container.config.get(entry.key),
throwsA(isA<ConfigurationException>()),
reason: 'Required field ${entry.key} should throw when missing',
);
}
// Test default values
if (field.containsKey('default')) {
_container.config.set(entry.key, null);
final defaultValue = _container.config.get(entry.key);
expect(defaultValue, equals(field['default']));
}
}
});
});
group('Database Integration', () {
test('should create required tables', () async {
final tables = module.manifest.databaseTables;
for (final table in tables) {
final exists = await _testDb.tableExists(table);
expect(exists, isTrue, reason: 'Table $table should exist');
}
});
test('should run migrations successfully', () async {
for (final migration in module.migrations) {
await migration.up(_testDb);
// Verify migration effects
expect(await _testDb.migrationExists(migration.id), isTrue);
}
});
});
group('API Endpoints', () {
for (final endpoint in module.manifest.exports['endpoints'] ?? []) {
test('should handle $endpoint requests', () async {
// Test endpoint functionality
await _testEndpoint(endpoint);
});
}
});
group('Error Handling', () {
test('should handle invalid requests gracefully', () async {
// Test error scenarios
await _testErrorScenarios(module);
});
});
group('Performance', () {
test('should meet performance requirements', () async {
await _testPerformance(module);
});
});
});
}
Future<void> _testEndpoint(String endpoint) async {
// Generate test request
final request = _generateTestRequest(endpoint);
// Execute endpoint
final server = _container.get<HypermodernServer>();
final response = await server.handleRequest(endpoint, request);
// Validate response
expect(response, isNotNull);
expect(response.statusCode, equals(200));
}
Future<void> _testErrorScenarios(HypermodernModule module) async {
// Test various error conditions
final errorScenarios = [
'invalid_request_data',
'missing_authentication',
'insufficient_permissions',
'external_service_unavailable',
];
for (final scenario in errorScenarios) {
await _testErrorScenario(scenario);
}
}
Future<void> _testPerformance(HypermodernModule module) async {
// Performance benchmarks
final benchmarks = {
'endpoint_response_time': Duration(milliseconds: 100),
'database_query_time': Duration(milliseconds: 50),
'memory_usage': 50 * 1024 * 1024, // 50MB
};
for (final entry in benchmarks.entries) {
final result = await _measurePerformance(entry.key);
expect(result, lessThan(entry.value), reason: '${entry.key} performance requirement not met');
}
}
}
// Usage example
void main() {
final testFramework = ModuleTestFramework(
container: TestModuleContainer(),
testDb: TestDatabase(),
mockServer: MockHttpServer(),
);
testFramework.testModule(ExampleModule());
}
Module Versioning and Dependencies
Semantic Versioning for Modules
class ModuleVersionManager {
static bool isCompatible(String moduleVersion, String requiredVersion) {
final module = Version.parse(moduleVersion);
final required = Version.parse(requiredVersion);
// Major version must match
if (module.major != required.major) {
return false;
}
// Minor version must be >= required
if (module.minor < required.minor) {
return false;
}
// Patch version must be >= required if minor versions match
if (module.minor == required.minor && module.patch < required.patch) {
return false;
}
return true;
}
static List<String> getBreakingChanges(String fromVersion, String toVersion) {
final from = Version.parse(fromVersion);
final to = Version.parse(toVersion);
final changes = <String>[];
if (to.major > from.major) {
changes.add('Major version upgrade - breaking changes expected');
}
if (to.minor > from.minor) {
changes.add('Minor version upgrade - new features added');
}
if (to.patch > from.patch) {
changes.add('Patch version upgrade - bug fixes');
}
return changes;
}
}
class DependencyResolver {
final ModuleRegistry _registry;
DependencyResolver(this._registry);
Future<List<ModuleDependency>> resolveDependencies(
Map<String, String> dependencies,
) async {
final resolved = <ModuleDependency>[];
final visited = <String>{};
for (final entry in dependencies.entries) {
await _resolveDependency(
entry.key,
entry.value,
resolved,
visited,
);
}
return _topologicalSort(resolved);
}
Future<void> _resolveDependency(
String moduleName,
String versionConstraint,
List<ModuleDependency> resolved,
Set<String> visited,
) async {
if (visited.contains(moduleName)) {
return; // Already processed
}
visited.add(moduleName);
// Find compatible version
final availableVersions = await _registry.getAvailableVersions(moduleName);
final compatibleVersion = _findCompatibleVersion(availableVersions, versionConstraint);
if (compatibleVersion == null) {
throw DependencyResolutionException(
'No compatible version found for $moduleName $versionConstraint',
);
}
// Get module info
final moduleInfo = await _registry.getModuleInfo(moduleName, compatibleVersion);
// Resolve transitive dependencies
for (final dep in moduleInfo.dependencies.entries) {
await _resolveDependency(dep.key, dep.value, resolved, visited);
}
// Add to resolved list
resolved.add(ModuleDependency(
name: moduleName,
version: compatibleVersion,
dependencies: moduleInfo.dependencies,
));
}
String? _findCompatibleVersion(List<String> versions, String constraint) {
// Parse constraint (e.g., "^1.0.0", ">=1.2.0 <2.0.0")
final constraintParser = VersionConstraintParser();
final parsedConstraint = constraintParser.parse(constraint);
// Find highest compatible version
final compatibleVersions = versions
.map(Version.parse)
.where(parsedConstraint.allows)
.toList()
..sort((a, b) => b.compareTo(a)); // Descending order
return compatibleVersions.isNotEmpty ? compatibleVersions.first.toString() : null;
}
List<ModuleDependency> _topologicalSort(List<ModuleDependency> dependencies) {
final sorted = <ModuleDependency>[];
final visited = <String>{};
final visiting = <String>{};
void visit(ModuleDependency dep) {
if (visiting.contains(dep.name)) {
throw DependencyResolutionException('Circular dependency detected: ${dep.name}');
}
if (visited.contains(dep.name)) {
return;
}
visiting.add(dep.name);
// Visit dependencies first
for (final depName in dep.dependencies.keys) {
final dependency = dependencies.firstWhere((d) => d.name == depName);
visit(dependency);
}
visiting.remove(dep.name);
visited.add(dep.name);
sorted.add(dep);
}
for (final dep in dependencies) {
visit(dep);
}
return sorted;
}
}
Contributing to the Ecosystem
Module Contribution Guidelines
class ModuleContributionGuide {
static const String contributionProcess = '''
1. Module Proposal
- Create RFC (Request for Comments) document
- Discuss with community on GitHub Discussions
- Get feedback from core maintainers
2. Development
- Follow module development best practices
- Implement comprehensive tests
- Write detailed documentation
- Follow code style guidelines
3. Review Process
- Submit pull request to module registry
- Code review by maintainers
- Security audit for sensitive modules
- Performance benchmarking
4. Publication
- Module published to registry
- Documentation added to website
- Announcement in community channels
- Version tagged and released
''';
static const List<String> qualityStandards = [
'Comprehensive test coverage (>90%)',
'Clear and detailed documentation',
'Semantic versioning compliance',
'Security best practices followed',
'Performance benchmarks provided',
'Backward compatibility maintained',
'Error handling and logging implemented',
'Configuration validation included',
];
static const Map<String, String> moduleCategories = {
'authentication': 'Authentication and authorization modules',
'database': 'Database integration and ORM modules',
'messaging': 'Message queues and pub/sub modules',
'storage': 'File storage and CDN modules',
'monitoring': 'Logging, metrics, and monitoring modules',
'payment': 'Payment processing modules',
'communication': 'Email, SMS, and notification modules',
'integration': 'Third-party service integration modules',
'utility': 'General utility and helper modules',
};
}
class ModuleQualityChecker {
static Future<QualityReport> checkModule(String modulePath) async {
final report = QualityReport();
// Check manifest
await _checkManifest(modulePath, report);
// Check code quality
await _checkCodeQuality(modulePath, report);
// Check tests
await _checkTests(modulePath, report);
// Check documentation
await _checkDocumentation(modulePath, report);
// Check security
await _checkSecurity(modulePath, report);
return report;
}
static Future<void> _checkManifest(String modulePath, QualityReport report) async {
final manifestFile = File(path.join(modulePath, 'module.json'));
if (!await manifestFile.exists()) {
report.addError('Module manifest (module.json) is missing');
return;
}
try {
final manifestContent = await manifestFile.readAsString();
final manifest = jsonDecode(manifestContent) as Map<String, dynamic>;
// Check required fields
final requiredFields = ['name', 'version', 'description', 'author'];
for (final field in requiredFields) {
if (!manifest.containsKey(field)) {
report.addError('Required manifest field missing: $field');
}
}
// Validate version format
try {
Version.parse(manifest['version'] as String);
} catch (e) {
report.addError('Invalid version format: ${manifest['version']}');
}
// Check configuration schema
if (manifest.containsKey('configuration')) {
_validateConfigurationSchema(manifest['configuration'], report);
}
} catch (e) {
report.addError('Invalid manifest JSON: $e');
}
}
static Future<void> _checkCodeQuality(String modulePath, QualityReport report) async {
// Run dart analyze
final analyzeResult = await Process.run('dart', ['analyze', modulePath]);
if (analyzeResult.exitCode != 0) {
report.addWarning('Code analysis issues found:\n${analyzeResult.stdout}');
}
// Check for TODO/FIXME comments
final dartFiles = await _findDartFiles(modulePath);
for (final file in dartFiles) {
final content = await File(file).readAsString();
if (content.contains('TODO') || content.contains('FIXME')) {
report.addWarning('TODO/FIXME comments found in $file');
}
}
}
static Future<void> _checkTests(String modulePath, QualityReport report) async {
final testDir = Directory(path.join(modulePath, 'test'));
if (!await testDir.exists()) {
report.addError('Test directory is missing');
return;
}
// Run tests
final testResult = await Process.run('dart', ['test', modulePath]);
if (testResult.exitCode != 0) {
report.addError('Tests are failing:\n${testResult.stdout}');
}
// Check test coverage
final coverageResult = await Process.run('dart', [
'test',
'--coverage=coverage',
modulePath,
]);
if (coverageResult.exitCode == 0) {
final coverage = await _calculateCoverage(path.join(modulePath, 'coverage'));
if (coverage < 90.0) {
report.addWarning('Test coverage is below 90%: ${coverage.toStringAsFixed(1)}%');
}
}
}
static Future<void> _checkDocumentation(String modulePath, QualityReport report) async {
// Check README
final readmeFile = File(path.join(modulePath, 'README.md'));
if (!await readmeFile.exists()) {
report.addError('README.md is missing');
} else {
final content = await readmeFile.readAsString();
if (content.length < 500) {
report.addWarning('README.md is too short (less than 500 characters)');
}
// Check for required sections
final requiredSections = ['Installation', 'Usage', 'Configuration'];
for (final section in requiredSections) {
if (!content.toLowerCase().contains(section.toLowerCase())) {
report.addWarning('README.md missing section: $section');
}
}
}
// Check CHANGELOG
final changelogFile = File(path.join(modulePath, 'CHANGELOG.md'));
if (!await changelogFile.exists()) {
report.addWarning('CHANGELOG.md is missing');
}
// Check LICENSE
final licenseFile = File(path.join(modulePath, 'LICENSE'));
if (!await licenseFile.exists()) {
report.addError('LICENSE file is missing');
}
}
static Future<void> _checkSecurity(String modulePath, QualityReport report) async {
// Check for hardcoded secrets
final dartFiles = await _findDartFiles(modulePath);
final secretPatterns = [
RegExp(r'password\s*=\s*["\'][^"\']+["\']', caseSensitive: false),
RegExp(r'api[_-]?key\s*=\s*["\'][^"\']+["\']', caseSensitive: false),
RegExp(r'secret\s*=\s*["\'][^"\']+["\']', caseSensitive: false),
];
for (final file in dartFiles) {
final content = await File(file).readAsString();
for (final pattern in secretPatterns) {
if (pattern.hasMatch(content)) {
report.addError('Potential hardcoded secret found in $file');
}
}
}
// Check dependencies for known vulnerabilities
await _checkDependencyVulnerabilities(modulePath, report);
}
}
What's Next
You now understand the rich ecosystem of Hypermodern modules and how to create, test, and contribute your own modules. The final chapter will cover integration patterns, showing you how to integrate Hypermodern with existing systems, migrate from other frameworks, and build hybrid architectures.
No Comments