cloud-sql/mysql/mysql2/index.js (152 lines of code) (raw):

// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. 'use strict'; const express = require('express'); const createConnectorIAMAuthnPool = require('./connect-connector-auto-iam-authn.js'); const createConnectorPool = require('./connect-connector.js'); const createTcpPool = require('./connect-tcp.js'); const createUnixSocketPool = require('./connect-unix.js'); const app = express(); app.set('view engine', 'pug'); app.enable('trust proxy'); // Automatically parse request body as form data. app.use(express.urlencoded({extended: false})); // This middleware is available in Express v4.16.0 onwards app.use(express.json()); // Set Content-Type for all responses for these routes. app.use((req, res, next) => { res.set('Content-Type', 'text/html'); next(); }); // Create a Winston logger that streams to Stackdriver Logging. const winston = require('winston'); const {LoggingWinston} = require('@google-cloud/logging-winston'); const loggingWinston = new LoggingWinston(); const logger = winston.createLogger({ level: 'info', transports: [new winston.transports.Console(), loggingWinston], }); // Retrieve and return a specified secret from Secret Manager const {SecretManagerServiceClient} = require('@google-cloud/secret-manager'); const client = new SecretManagerServiceClient(); async function accessSecretVersion(secretName) { const [version] = await client.accessSecretVersion({name: secretName}); return version.payload.data; } const createPool = async () => { const config = { // [START cloud_sql_mysql_mysql2_limit] // 'connectionLimit' is the maximum number of connections the pool is allowed // to keep at once. connectionLimit: 5, // [END cloud_sql_mysql_mysql2_limit] // [START cloud_sql_mysql_mysql2_timeout] // 'connectTimeout' is the maximum number of milliseconds before a timeout // occurs during the initial connection to the database. connectTimeout: 10000, // 10 seconds // 'acquireTimeout' is currently unsupported by mysql2 // 'waitForConnections' determines the pool's action when no connections are // free. If true, the request will queued and a connection will be presented // when ready. If false, the pool will call back with an error. waitForConnections: true, // Default: true // 'queueLimit' is the maximum number of requests for connections the pool // will queue at once before returning an error. If 0, there is no limit. queueLimit: 0, // Default: 0 // [END cloud_sql_mysql_mysql2_timeout] // [START cloud_sql_mysql_mysql2_backoff] // The mysql module automatically uses exponential delays between failed // connection attempts. // [END cloud_sql_mysql_mysql2_backoff] }; // Check if a Secret Manager secret version is defined // If a version is defined, retrieve the secret from Secret Manager and set as the DB_PASS const {CLOUD_SQL_CREDENTIALS_SECRET} = process.env; if (CLOUD_SQL_CREDENTIALS_SECRET) { const secrets = await accessSecretVersion(CLOUD_SQL_CREDENTIALS_SECRET); try { process.env.DB_PASS = secrets.toString(); } catch (err) { err.message = `Unable to parse secret from Secret Manager. Make sure that the secret is JSON formatted: \n ${err.message} `; throw err; } } if (process.env.INSTANCE_CONNECTION_NAME) { // Uses the Cloud SQL Node.js Connector when INSTANCE_CONNECTION_NAME // (e.g., project:region:instance) is defined if (process.env.DB_IAM_USER) { // Either a DB_USER or a DB_IAM_USER should be defined. If both are // defined, DB_IAM_USER takes precedence return createConnectorIAMAuthnPool(config); } else { return createConnectorPool(config); } } else if (process.env.INSTANCE_HOST) { // Use a TCP socket when INSTANCE_HOST (e.g., 127.0.0.1) is defined return createTcpPool(config); } else if (process.env.INSTANCE_UNIX_SOCKET) { // Use a Unix socket when INSTANCE_UNIX_SOCKET (e.g., /cloudsql/proj:region:instance) is defined. return createUnixSocketPool(config); } else { throw 'Set either `INSTANCE_CONNECTION_NAME` or `INSTANCE_HOST` or `INSTANCE_UNIX_SOCKET` environment variables.'; } }; const ensureSchema = async pool => { // Wait for tables to be created (if they don't already exist). await pool.query( `CREATE TABLE IF NOT EXISTS votes ( vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, candidate CHAR(6) NOT NULL, PRIMARY KEY (vote_id) );` ); console.log("Ensured that table 'votes' exists"); }; const createPoolAndEnsureSchema = async () => await createPool() .then(async pool => { await ensureSchema(pool); return pool; }) .catch(err => { logger.error(err); throw err; }); // Set up a variable to hold our connection pool. It would be safe to // initialize this right away, but we defer its instantiation to ease // testing different configurations. let pool; app.use(async (req, res, next) => { if (pool) { return next(); } try { pool = await createPoolAndEnsureSchema(); next(); } catch (err) { logger.error(err); return next(err); } }); // Serve the index page, showing vote tallies. const httpGet = app.get('/', async (req, res) => { pool = pool || (await createPoolAndEnsureSchema()); try { // Get the 5 most recent votes. const recentVotesQuery = pool.query( 'SELECT candidate, time_cast FROM votes ORDER BY time_cast DESC LIMIT 5' ); // Get votes const stmt = 'SELECT COUNT(vote_id) as count FROM votes WHERE candidate=?'; const tabsQuery = pool.query(stmt, ['TABS']); const spacesQuery = pool.query(stmt, ['SPACES']); // Run queries concurrently, and wait for them to complete // This is faster than await-ing each query object as it is created const [recentVotes] = await recentVotesQuery; // Return only the results, not the field metadata const [tabsVotes] = await tabsQuery; const [spacesVotes] = await spacesQuery; res.render('index.pug', { recentVotes, tabCount: tabsVotes.count, spaceCount: spacesVotes.count, }); } catch (err) { logger.error(err); res .status(500) .send( 'Unable to load page. Please check the application logs for more details.' ) .end(); } }); // Handle incoming vote requests and inserting them into the database. const httpPost = app.post('*', async (req, res) => { const {team} = req.body; const timestamp = new Date(); if (!team || (team !== 'TABS' && team !== 'SPACES')) { return res.status(400).send('Invalid team specified.').end(); } pool = pool || (await createPoolAndEnsureSchema()); // [START cloud_sql_mysql_mysql2_connection] try { const stmt = 'INSERT INTO votes (time_cast, candidate) VALUES (?, ?)'; // Pool.query automatically checks out, uses, and releases a connection // back into the pool, ensuring it is always returned successfully. await pool.query(stmt, [timestamp, team]); } catch (err) { // If something goes wrong, handle the error in this section. This might // involve retrying or adjusting parameters depending on the situation. // [START_EXCLUDE] logger.error(err); return res .status(500) .send( 'Unable to successfully cast vote! Please check the application logs for more details.' ) .end(); // [END_EXCLUDE] } // [END cloud_sql_mysql_mysql2_connection] res.status(200).send(`Successfully voted for ${team} at ${timestamp}`).end(); }); /** * Responds to GET and POST requests for TABS vs SPACES sample app. * * @param {Object} req Cloud Function request context. * @param {Object} res Cloud Function response context. */ exports.votes = (req, res) => { switch (req.method) { case 'GET': httpGet(req, res); break; case 'POST': httpPost(req, res); break; default: res.status(405).send({error: 'Something blew up!'}); break; } }; module.exports.app = app;