packages/fxa-profile-server/lib/db/mysql/index.js (196 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const mysql = require('mysql'); const buf = require('buf').hex; const AppError = require('../../error'); const config = require('../../config'); const logger = require('../../logging')('db.mysql'); const P = require('../../promise'); const REQUIRED_SQL_MODES = ['STRICT_ALL_TABLES', 'NO_ENGINE_SUBSTITUTION']; const REQUIRED_CHARSET = 'UTF8MB4_BIN'; function MysqlStore(options) { if (options.charset && options.charset !== REQUIRED_CHARSET) { logger.error('createDatabase.invalidCharset', { charset: options.charset }); throw new Error('You cannot use any charset besides ' + REQUIRED_CHARSET); } else { options.charset = REQUIRED_CHARSET; } options.typeCast = function (field, next) { if (field.type === 'TINY' && field.length === 1) { return field.string() === '1'; } return next(); }; this._pool = mysql.createPool(options); } MysqlStore.connect = function mysqlConnect(options) { return Promise.resolve(new MysqlStore(options)); }; const Q_AVATAR_INSERT = 'INSERT INTO avatars (id, url, userId, providerId) ' + 'VALUES (?, ?, ?, ?)'; const Q_AVATAR_UPDATE = 'INSERT INTO avatar_selected (userId, avatarId) ' + 'VALUES (?, ?) ON DUPLICATE KEY UPDATE avatarId = VALUES(avatarId)'; const Q_AVATAR_GET = 'SELECT * FROM avatars WHERE id=?'; const Q_SELECTED_AVATAR = 'SELECT avatars.* FROM avatars LEFT JOIN ' + 'avatar_selected ON (avatars.id = avatar_selected.avatarId) WHERE ' + 'avatars.userId=? AND avatar_selected.avatarId IS NOT NULL'; const Q_AVATAR_DELETE = 'DELETE FROM avatars WHERE id=?'; const Q_USER_AVATARS_DELETE = 'DELETE FROM avatars where userId=?'; const Q_PROVIDER_INSERT = 'INSERT INTO avatar_providers (name) VALUES (?)'; const Q_PROVIDER_GET_BY_NAME = 'SELECT * FROM avatar_providers WHERE name=?'; const Q_PROVIDER_GET_BY_ID = 'SELECT * FROM avatar_providers WHERE id=?'; const Q_PROFILE_DISPLAY_NAME_UPDATE = 'INSERT INTO profile ' + '(userId, displayName) VALUES (?, ?) ON DUPLICATE KEY UPDATE ' + 'displayName = VALUES(displayName)'; const Q_PROFILE_DISPLAY_NAME_GET = 'SELECT displayName FROM profile ' + 'WHERE userId=?'; const Q_PROFILE_DELETE = 'DELETE FROM profile WHERE userId=?'; function firstRow(rows) { return rows[0]; } function releaseConn(connection) { connection.release(); } MysqlStore.prototype = { ping: function ping() { logger.debug('ping'); // see bluebird.using(): // https://github.com/petkaantonov/bluebird/blob/master/API.md#resource-management return P.using(this._getConnection(), function (conn) { return new P(function (resolve, reject) { conn.ping(function (err) { if (err) { logger.error('ping', err); reject(err); } else { resolve(); } }); }); }); }, addAvatar: function addAvatar(id, uid, url, provider) { id = buf(id); uid = buf(uid); var store = this; return this.getProviderByName(provider).then(function (prov) { if (!prov) { throw AppError.unsupportedProvider(url); } return store ._write(Q_AVATAR_INSERT, [id, url, uid, prov.id]) .then(function () { // always select the newly uploaded avatar return store._write(Q_AVATAR_UPDATE, [uid, id]); }); }); }, getAvatar: function getAvatar(id) { return this._readOne(Q_AVATAR_GET, [buf(id)]); }, getSelectedAvatar: function getSelectedAvatar(uid) { return this._readOne(Q_SELECTED_AVATAR, [buf(uid)]); }, deleteAvatar: function deleteAvatar(id) { return this._write(Q_AVATAR_DELETE, [buf(id)]); }, deleteUserAvatars: function deleteUserAvatars(uid) { return this._write(Q_USER_AVATARS_DELETE, [buf(uid)]); }, addProvider: function addProvider(name) { return this._write(Q_PROVIDER_INSERT, [name]); }, getProviderByName: function getProviderByName(name) { return this._readOne(Q_PROVIDER_GET_BY_NAME, [name]); }, getProviderById: function getProviderById(id) { return this._readOne(Q_PROVIDER_GET_BY_ID, [id]); }, setDisplayName: function setDisplayName(uid, displayName) { return this._write(Q_PROFILE_DISPLAY_NAME_UPDATE, [buf(uid), displayName]); }, getDisplayName: function getDisplayName(uid) { return this._readOne(Q_PROFILE_DISPLAY_NAME_GET, [buf(uid)]); }, removeProfile: function removeProfile(uid) { return this._write(Q_PROFILE_DELETE, [buf(uid)]); }, _write: function _write(sql, params) { return this._query(sql, params); }, _read: function _read(sql, params) { return this._query(sql, params); }, _readOne: function _readOne(sql, params) { return this._read(sql, params).then(firstRow); }, _getConnection: function _getConnection() { // see bluebird.using()/disposer(): // https://github.com/petkaantonov/bluebird/blob/master/API.md#resource-management // // tl;dr: using() and disposer() ensures that the dispose method will // ALWAYS be called at the end of the promise stack, regardless of // various errors thrown. So this should ALWAYS release the connection. var pool = this._pool; return new P(function (resolve, reject) { pool.getConnection(function (err, conn) { if (err) { return reject(err); } if (conn._fxa_initialized) { return resolve(conn); } // Enforce sane defaults on every new connection. // These *should* be set by the database by default, but it's nice // to have an additional layer of protection here. const query = P.promisify(conn.query, { context: conn }); return resolve( (async () => { // Always communicate timestamps in UTC. await query("SET time_zone = '+00:00'"); // Always use full 4-byte UTF-8 for communicating unicode. await query('SET NAMES utf8mb4 COLLATE utf8mb4_bin;'); // Always have certain modes active. The complexity here is to // preserve any extra modes active by default on the server. // We also try to preserve the order of the existing mode flags, // just in case the order has some obscure effect we don't know about. const rows = await query('SELECT @@sql_mode AS mode'); const modes = rows[0]['mode'].split(','); let needToSetMode = false; for (const requiredMode of REQUIRED_SQL_MODES) { if (modes.indexOf(requiredMode) === -1) { modes.push(requiredMode); needToSetMode = true; } } if (needToSetMode) { const mode = modes.join(','); await query("SET SESSION sql_mode = '" + mode + "'"); } // Avoid repeating all that work for existing connections. conn._fxa_initialized = true; return conn; })() ); }); }).disposer(releaseConn); }, _query: function _query(sql, params) { return P.using(this._getConnection(), function (conn) { return new P(function (resolve, reject) { conn.query(sql, params || [], function (err, results) { if (err) { reject(err); } else { resolve(results); } }); }); }); }, disconnect: function disconnect() { return new P((resolve, reject) => { this._pool.end((err) => { if (err) { return reject(err); } return resolve(); }); }); }, }; if (config.get('env') === 'test') { MysqlStore.prototype._clear = function clear() { var store = this; return this._write('DELETE FROM avatar_selected;') .then(function () { return store._write('DELETE FROM avatars;'); }) .then(function () { return store._write('DELETE FROM avatar_providers;'); }); }; } module.exports = MysqlStore;