diff --git a/app/lib/database/database.dart b/app/lib/database/database.dart new file mode 100644 index 0000000000..358c96600f --- /dev/null +++ b/app/lib/database/database.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:gcloud/service_scope.dart' as ss; +import 'package:meta/meta.dart'; +import 'package:postgres/postgres.dart'; +import 'package:pub_dev/service/secret/backend.dart'; +import 'package:pub_dev/shared/env_config.dart'; + +/// Sets the primary database service. +void registerPrimaryDatabase(PrimaryDatabase database) => + ss.register(#_primaryDatabase, database); + +/// The active primary database service. +PrimaryDatabase? get primaryDatabase => + ss.lookup(#_primaryDatabase) as PrimaryDatabase?; + +/// Access to the primary database connection and object mapping. +class PrimaryDatabase { + final Pool _pg; + + PrimaryDatabase._(this._pg); + + /// Gets the connection string either from the environment variable or from + /// the secret backend, connects to it and registers the primary database + /// service in the current scope. + static Future tryRegisterInScope() async { + final connectionString = + envConfig.pubPostgresUrl ?? + (await secretBackend.lookup(SecretKey.postgresConnectionString)); + if (connectionString == null) { + // ignore for now, must throw once we have the environment setup ready + return; + } + final database = await _fromConnectionString(connectionString); + registerPrimaryDatabase(database); + ss.registerScopeExitCallback(database.close); + } + + static Future _fromConnectionString(String value) async { + final pg = Pool.withUrl(value); + return PrimaryDatabase._(pg); + } + + Future close() async { + await _pg.close(); + } + + @visibleForTesting + Future verifyConnection() async { + final rs = await _pg.execute('SELECT 1'); + if (rs.length != 1) { + throw StateError('Connection is not returning expected rows.'); + } + } +} diff --git a/app/lib/service/secret/models.dart b/app/lib/service/secret/models.dart index 6b0d787f82..a39a926523 100644 --- a/app/lib/service/secret/models.dart +++ b/app/lib/service/secret/models.dart @@ -4,6 +4,10 @@ /// Identifiers and secret keys. abstract class SecretKey { + /// Postgres connection string. + static const String postgresConnectionString = 'postgres-connection-string'; + + /// Redis connection string. static const String redisConnectionString = 'redis-connection-string'; /// OAuth client secret. @@ -27,6 +31,7 @@ abstract class SecretKey { /// List of all keys. static const values = [ + postgresConnectionString, redisConnectionString, oauthClientSecret, announcement, diff --git a/app/lib/service/services.dart b/app/lib/service/services.dart index dc71d8c91c..af9863a9ae 100644 --- a/app/lib/service/services.dart +++ b/app/lib/service/services.dart @@ -14,6 +14,7 @@ import 'package:gcloud/service_scope.dart'; import 'package:gcloud/storage.dart'; import 'package:googleapis_auth/auth_io.dart' as auth; import 'package:logging/logging.dart'; +import 'package:pub_dev/database/database.dart'; import 'package:pub_dev/package/api_export/api_exporter.dart'; import 'package:pub_dev/search/handlers.dart'; import 'package:pub_dev/service/async_queue/async_queue.dart'; @@ -250,6 +251,7 @@ Future _withPubServices(FutureOr Function() fn) async { await storageService.verifyBucketExistenceAndAccess(bucketName); } + await PrimaryDatabase.tryRegisterInScope(); registerAccountBackend(AccountBackend(dbService)); registerAdminBackend(AdminBackend(dbService)); registerAnnouncementBackend(AnnouncementBackend()); diff --git a/app/test/shared/postgresql_ci_test.dart b/app/test/database/postgresql_ci_test.dart similarity index 74% rename from app/test/shared/postgresql_ci_test.dart rename to app/test/database/postgresql_ci_test.dart index d5df7fa44f..f8e5f03419 100644 --- a/app/test/shared/postgresql_ci_test.dart +++ b/app/test/database/postgresql_ci_test.dart @@ -4,9 +4,12 @@ import 'package:clock/clock.dart'; import 'package:postgres/postgres.dart'; +import 'package:pub_dev/database/database.dart'; import 'package:pub_dev/shared/env_config.dart'; import 'package:test/test.dart'; +import '../shared/test_services.dart'; + void main() { group('Postgresql connection on CI', () { test('connects to CI instance', () async { @@ -32,5 +35,17 @@ void main() { await conn.execute('CREATE DATABASE $dbName'); await conn.execute('DROP DATABASE $dbName'); }); + + testWithProfile( + 'registered database scope', + fn: () async { + final pubPostgresUrl = envConfig.pubPostgresUrl; + if (pubPostgresUrl == null) { + markTestSkipped('PUB_POSTGRES_URL was not specified.'); + return; + } + await primaryDatabase!.verifyConnection(); + }, + ); }); }