Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions app/lib/database/database.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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<PrimaryDatabase> _fromConnectionString(String value) async {
final pg = Pool.withUrl(value);
return PrimaryDatabase._(pg);
}

Future<void> close() async {
await _pg.close();
}

@visibleForTesting
Future<void> verifyConnection() async {
final rs = await _pg.execute('SELECT 1');
if (rs.length != 1) {
throw StateError('Connection is not returning expected rows.');
}
}
}
5 changes: 5 additions & 0 deletions app/lib/service/secret/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -27,6 +31,7 @@ abstract class SecretKey {

/// List of all keys.
static const values = [
postgresConnectionString,
redisConnectionString,
oauthClientSecret,
announcement,
Expand Down
2 changes: 2 additions & 0 deletions app/lib/service/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -250,6 +251,7 @@ Future<R> _withPubServices<R>(FutureOr<R> Function() fn) async {
await storageService.verifyBucketExistenceAndAccess(bucketName);
}

await PrimaryDatabase.tryRegisterInScope();
registerAccountBackend(AccountBackend(dbService));
registerAdminBackend(AdminBackend(dbService));
registerAnnouncementBackend(AnnouncementBackend());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
},
);
});
}