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;