database.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import pg from 'pg';
  2. import pgConnectionString from 'pg-connection-string';
  3. import { parseIntFromEnvValue } from './utils.js';
  4. /**
  5. * @param {NodeJS.ProcessEnv} env the `process.env` value to read configuration from
  6. * @param {string} environment
  7. * @returns {pg.PoolConfig} the configuration for the PostgreSQL connection
  8. */
  9. export function configFromEnv(env, environment) {
  10. /** @type {Record<string, pg.PoolConfig>} */
  11. const pgConfigs = {
  12. development: {
  13. user: env.DB_USER || pg.defaults.user,
  14. password: env.DB_PASS || pg.defaults.password,
  15. database: env.DB_NAME || 'mastodon_development',
  16. host: env.DB_HOST || pg.defaults.host,
  17. port: parseIntFromEnvValue(env.DB_PORT, pg.defaults.port ?? 5432, 'DB_PORT')
  18. },
  19. production: {
  20. user: env.DB_USER || 'mastodon',
  21. password: env.DB_PASS || '',
  22. database: env.DB_NAME || 'mastodon_production',
  23. host: env.DB_HOST || 'localhost',
  24. port: parseIntFromEnvValue(env.DB_PORT, 5432, 'DB_PORT')
  25. },
  26. };
  27. /**
  28. * @type {pg.PoolConfig}
  29. */
  30. let baseConfig = {};
  31. if (env.DATABASE_URL) {
  32. const parsedUrl = pgConnectionString.parse(env.DATABASE_URL);
  33. // The result of dbUrlToConfig from pg-connection-string is not type
  34. // compatible with pg.PoolConfig, since parts of the connection URL may be
  35. // `null` when pg.PoolConfig expects `undefined`, as such we have to
  36. // manually create the baseConfig object from the properties of the
  37. // parsedUrl.
  38. //
  39. // For more information see:
  40. // https://github.com/brianc/node-postgres/issues/2280
  41. //
  42. // FIXME: clean up once brianc/node-postgres#3128 lands
  43. if (typeof parsedUrl.password === 'string') baseConfig.password = parsedUrl.password;
  44. if (typeof parsedUrl.host === 'string') baseConfig.host = parsedUrl.host;
  45. if (typeof parsedUrl.user === 'string') baseConfig.user = parsedUrl.user;
  46. if (typeof parsedUrl.port === 'string') {
  47. const parsedPort = parseInt(parsedUrl.port, 10);
  48. if (isNaN(parsedPort)) {
  49. throw new Error('Invalid port specified in DATABASE_URL environment variable');
  50. }
  51. baseConfig.port = parsedPort;
  52. }
  53. if (typeof parsedUrl.database === 'string') baseConfig.database = parsedUrl.database;
  54. if (typeof parsedUrl.options === 'string') baseConfig.options = parsedUrl.options;
  55. // The pg-connection-string type definition isn't correct, as parsedUrl.ssl
  56. // can absolutely be an Object, this is to work around these incorrect
  57. // types, including the casting of parsedUrl.ssl to Record<string, any>
  58. if (typeof parsedUrl.ssl === 'boolean') {
  59. baseConfig.ssl = parsedUrl.ssl;
  60. } else if (typeof parsedUrl.ssl === 'object' && !Array.isArray(parsedUrl.ssl) && parsedUrl.ssl !== null) {
  61. /** @type {Record<string, any>} */
  62. const sslOptions = parsedUrl.ssl;
  63. baseConfig.ssl = {};
  64. baseConfig.ssl.cert = sslOptions.cert;
  65. baseConfig.ssl.key = sslOptions.key;
  66. baseConfig.ssl.ca = sslOptions.ca;
  67. baseConfig.ssl.rejectUnauthorized = sslOptions.rejectUnauthorized;
  68. }
  69. // Support overriding the database password in the connection URL
  70. if (!baseConfig.password && env.DB_PASS) {
  71. baseConfig.password = env.DB_PASS;
  72. }
  73. } else if (Object.hasOwn(pgConfigs, environment)) {
  74. baseConfig = pgConfigs[environment];
  75. if (env.DB_SSLMODE) {
  76. switch(env.DB_SSLMODE) {
  77. case 'disable':
  78. case '':
  79. baseConfig.ssl = false;
  80. break;
  81. case 'no-verify':
  82. baseConfig.ssl = { rejectUnauthorized: false };
  83. break;
  84. default:
  85. baseConfig.ssl = {};
  86. break;
  87. }
  88. }
  89. } else {
  90. throw new Error('Unable to resolve postgresql database configuration.');
  91. }
  92. return {
  93. ...baseConfig,
  94. max: parseIntFromEnvValue(env.DB_POOL, 10, 'DB_POOL'),
  95. connectionTimeoutMillis: 15000,
  96. // Deliberately set application_name to an empty string to prevent excessive
  97. // CPU usage with PG Bouncer. See:
  98. // - https://github.com/mastodon/mastodon/pull/23958
  99. // - https://github.com/pgbouncer/pgbouncer/issues/349
  100. application_name: '',
  101. };
  102. }
  103. let pool;
  104. /**
  105. *
  106. * @param {pg.PoolConfig} config
  107. * @param {string} environment
  108. * @param {import('pino').Logger} logger
  109. * @returns {pg.Pool}
  110. */
  111. export function getPool(config, environment, logger) {
  112. if (pool) {
  113. return pool;
  114. }
  115. pool = new pg.Pool(config);
  116. // Setup logging on pool.query and client.query for checked out clients:
  117. // This is taken from: https://node-postgres.com/guides/project-structure
  118. if (environment === 'development') {
  119. const logQuery = (originalQuery) => {
  120. return async (queryTextOrConfig, values, ...rest) => {
  121. const start = process.hrtime();
  122. const result = await originalQuery.apply(pool, [queryTextOrConfig, values, ...rest]);
  123. const duration = process.hrtime(start);
  124. const durationInMs = (duration[0] * 1000000000 + duration[1]) / 1000000;
  125. logger.debug({
  126. query: queryTextOrConfig,
  127. values,
  128. duration: durationInMs
  129. }, 'Executed database query');
  130. return result;
  131. };
  132. };
  133. pool.on('connect', (client) => {
  134. const originalQuery = client.query.bind(client);
  135. client.query = logQuery(originalQuery);
  136. });
  137. }
  138. return pool;
  139. }