oaeproject/Hilary

View on GitHub
packages/oae-tenants/lib/internal/dao.networks.js

Summary

Maintainability
A
1 hr
Test Coverage
A
92%
/*!
 * Copyright 2014 Apereo Foundation (AF) Licensed under the
 * Educational Community 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://opensource.org/licenses/ECL-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.
 */

/* eslint-disable unicorn/no-array-callback-reference */
import { callbackify, format } from 'node:util';
import _ from 'underscore';
import clone from 'clone';
import ShortId from 'shortid';

import { logger } from 'oae-logger';

import { constructUpsertCQL, runBatchQuery, rowToHash, runQuery } from 'oae-util/lib/cassandra.js';
import * as EmitterAPI from 'oae-emitter';
import * as Pubsub from 'oae-util/lib/pubsub.js';

import { TenantNetwork } from '../model.js';

const log = logger('oae-tenants');

// A cache that holds all tenant networks keyed by tenantNetworkId
let _cacheTenantNetworks = null;

// An cache that holds all tenantAliases that belong to each tenant network, keyed by tenant network id
let _cacheTenantAliasesByTenantNetworkId = null;

// An cache that holds all network ids a tenant belongs to, keyed by tenant alias (inverse
// of _tenantAliasesByTenantNetworkId)
let _cacheTenantNetworkIdsByTenantAlias = null;

// Emit events when the cache was successfully invalidated
const emitter = new EmitterAPI.EventEmitter();

/**
 * Initialize the Tenant Networks DAO.
 */
const init = function () {
  // When an invalidate pubsub message comes in for tenant networks, clear
  // the local tenant networks caches
  Pubsub.emitter.on('oae-tenant-networks', (message) => {
    if (message === 'invalidate') {
      _invalidateLocalCache();
    }
  });
};

/// ////////////////////////////////
// OPERATIONS ON TENANT NETWORKS //
/// ////////////////////////////////

/**
 * Create a tenant network in the system.
 *
 * @param  {String}         displayName             The display name of the tenant network
 * @param  {Function}       callback                Standard callback function
 * @param  {Object}         callback.err            An error that occurred, if any
 * @param  {TenantNetwork}  callback.tenantNetwork  The tenant network that was created
 */
const createTenantNetwork = function (displayName, callback) {
  const tenantNetwork = new TenantNetwork(ShortId.generate(), displayName);

  // Create a query that can be used to create the tenant network
  const tenantNetworkQuery = constructUpsertCQL('TenantNetwork', 'id', tenantNetwork.id, _.omit(tenantNetwork, 'id'));
  if (!tenantNetworkQuery) {
    log().error(
      {
        err: new Error('Error creating a query to create a tenant network'),
        tenantNetwork
      },
      'Error creating a tenant network'
    );
    return callback({
      code: 500,
      msg: 'An unexpected error occurred while creating the tenant network'
    });
  }

  // Execute the create query
  callbackify(runQuery)(tenantNetworkQuery.query, tenantNetworkQuery.parameters, (error) => {
    if (error) {
      return callback(error);
    }

    _invalidateAllCaches();
    return callback(null, tenantNetwork);
  });
};

/**
 * Fetch all tenant networks from the system.
 *
 * @param  {Function}   callback                    Standard callback function
 * @param  {Object}     callback.err                An error that occurred, if any
 * @param  {Object}     callback.tenantNetworks     All tenant networks in the system, keyed by their tenant network id
 */
const getAllTenantNetworks = function (callback) {
  // Verify the cache is populated
  _ensureCache((error) => {
    if (error) {
      return callback(error);
    }

    return callback(null, clone(_cacheTenantNetworks));
  });
};

/**
 * Get a tenant network.
 *
 * @param  {String}         id                      The id of the tenant network to fetch
 * @param  {Function}       callback                Standard callback function
 * @param  {Object}         callback.err            An error that occurred, if any
 * @param  {TenantNetwork}  callback.tenantNetwork  The tenant network with the provided id
 */
const getTenantNetwork = function (id, callback) {
  _ensureCache((error) => {
    if (error) {
      return callback(error);
    }

    if (!_cacheTenantNetworks[id]) {
      return callback({
        code: 404,
        msg: format('Attempted to access non-existing tenant network: "%s"', id)
      });
    }

    return callback(null, clone(_cacheTenantNetworks[id]));
  });
};

/**
 * Update a tenant network.
 *
 * @param  {String}         id                      The id of the tenant network being updated
 * @param  {String}         displayName             The updated display name of the tenant network
 * @param  {Function}       callback                Standard callback function
 * @param  {Object}         callback.err            An error that occurred, if any
 * @param  {TenantNetwork}  callback.tenantNetwork  The new tenant network, after update
 */
const updateTenantNetwork = function (id, displayName, callback) {
  // Ensure the tenant network we're updating exists
  getTenantNetwork(id, (error, tenantNetwork) => {
    if (error) {
      return callback(error);
    }

    const query = constructUpsertCQL('TenantNetwork', 'id', id, { displayName });
    callbackify(runQuery)(query.query, query.parameters, (error_) => {
      if (error_) {
        return callback(error_);
      }

      _invalidateAllCaches();

      // Return the tenant network with the updates applied as a new object
      return callback(null, _.extend({}, tenantNetwork, { displayName }));
    });
  });
};

/**
 * Delete a tenant network from the system.
 *
 * @param  {String}     id              The id of the tenant network to delete
 * @param  {Function}   callback        Standard callback function
 * @param  {Object}     callback.err    An error that occurred, if any
 */
const deleteTenantNetwork = function (id, callback) {
  // Ensure the tenant network exists
  // eslint-disable-next-line no-unused-vars
  getTenantNetwork(id, (error, tenantNetwork) => {
    if (error) {
      return callback(error);
    }

    // Delete the tenant network and the associations to the child tenants
    const deleteQueries = [
      {
        query: 'DELETE FROM "TenantNetwork" WHERE "id" = ?',
        parameters: [id]
      },
      {
        query: 'DELETE FROM "TenantNetworkTenants" WHERE "tenantNetworkId" = ?',
        parameters: [id]
      }
    ];

    callbackify(runBatchQuery)(deleteQueries, (error_) => {
      if (error_) return callback(error_);

      _invalidateAllCaches();
      return callback();
    });
  });
};

/// ///////////////////////////////////////////////
// OPERATIONS ON TENANT NETWORK TENANTS ALIASES //
/// ///////////////////////////////////////////////

/**
 * Add the provided tenant aliases to the specified tenant network.
 *
 * @param  {String}     tenantNetworkId         The id of the tenant network to which to add the provided tenant aliases
 * @param  {String[]}   tenantAlises            The tenant aliases to add to the tenant network
 * @param  {Function}   callback                Standard callback function
 * @param  {Object}     callback.err            An error that occurred, if any
 */
const addTenantAliases = function (tenantNetworkId, tenantAliases, callback) {
  // Ensure the tenant network exists
  // eslint-disable-next-line no-unused-vars
  getTenantNetwork(tenantNetworkId, (error, tenantNetwork) => {
    if (error) {
      return callback(error);
    }

    if (_.isEmpty(tenantAliases)) {
      return callback();
    }

    // Map all tenant aliases into the queries necessary to insert them all into the tenant network tenants table
    const queries = _.map(tenantAliases, (tenantAlias) =>
      constructUpsertCQL('TenantNetworkTenants', ['tenantNetworkId', 'tenantAlias'], [tenantNetworkId, tenantAlias], {
        value: '1'
      })
    );

    callbackify(runBatchQuery)(queries, (error_) => {
      if (error_) return callback(error_);

      _invalidateAllCaches();
      return callback();
    });
  });
};

/**
 * Get all tenant network ids along with all the tenant aliases that belong to the network.
 *
 * @param  {Function}   callback                                Standard callback function
 * @param  {Object}     callback.err                            An error that occurred, if any
 * @param  {Object}     callback.tenantNetworkTenantAliases     An object keyed by tenant network id whose value is the array of tenant aliases that belong to the tenant network
 */
const getAllTenantNetworkTenantAliases = function (callback) {
  _ensureCache((error) => {
    if (error) {
      return callback(error);
    }

    return callback(null, clone(_cacheTenantAliasesByTenantNetworkId));
  });
};

/**
 * Remove the provided tenant aliases from the specified tenant network.
 *
 * @param  {String}     tenantNetworkId     The id of the tenant network from which to remove the provided tenant aliases
 * @param  {String[]}   tenantAlises        The tenant aliases to remove from the tenant network
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 */
const removeTenantAliases = function (tenantNetworkId, tenantAliases, callback) {
  // Ensure the tenant network exists
  // eslint-disable-next-line no-unused-vars
  getTenantNetwork(tenantNetworkId, (error, tenantNetwork) => {
    if (error) {
      return callback(error);
    }

    if (_.isEmpty(tenantAliases)) {
      return callback();
    }

    // Create and execute the delete queries
    const queries = _.map(tenantAliases, (tenantAlias) => ({
      query: 'DELETE FROM "TenantNetworkTenants" WHERE "tenantNetworkId" = ? AND "tenantAlias" = ?',
      parameters: [tenantNetworkId, tenantAlias]
    }));

    callbackify(runBatchQuery)(queries, (error_) => {
      if (error_) return callback(error_);

      _invalidateAllCaches();
      return callback();
    });
  });
};

/**
 * Fetch all tenant networks from Cassandra, ignoring the cache.
 *
 * @param  {Function}   callback                    Standard callback function
 * @param  {Object}     callback.err                An error that occurred, if any
 * @param  {Object}     callback.tenantNetworks     All tenant networks in cassandra keyed by their id
 * @api private
 */
const _getAllTenantNetworksFromCassandra = function (callback) {
  callbackify(runQuery)('SELECT * FROM "TenantNetwork"', null, (error, rows) => {
    if (error) {
      return callback(error);
    }

    // Convert all rows into tenant networks
    const tenantNetworks = _.chain(rows).map(_rowToTenantNetwork).compact().indexBy('id').value();
    return callback(null, tenantNetworks);
  });
};

/**
 * Fetch tenant network tenant alias associations from Cassandra for all provided tenant network ids, ignoring the cache.
 *
 * @param  {String[]}   tenantNetworkIds                The ids of the tenant networks whose tenant alias associations to fetch
 * @param  {Object}     callback.err                    An error that occurred, if any
 * @param  {Object}     callback.tenantNetworkAliases   An object keyed by tenant network id whose values are the arrays of tenant aliases that belong to the network
 * @api private
 */
const _getAllTenantNetworkTenantAliasesFromCassandra = function (tenantNetworkIds, callback) {
  if (_.isEmpty(tenantNetworkIds)) {
    return callback(null, {});
  }

  // Fetch all of the tenant aliases associated to the specified tenant network from Cassandra
  callbackify(runQuery)(
    'SELECT "tenantNetworkId", "tenantAlias" FROM "TenantNetworkTenants" WHERE "tenantNetworkId" IN ?',
    [tenantNetworkIds],
    (error, rows) => {
      if (error) {
        return callback(error);
      }

      // Collect all tenant network aliases for each tenant network
      const tenantNetworkAliases = {};
      _.chain(rows)
        .map(rowToHash)
        .each((rowHash) => {
          tenantNetworkAliases[rowHash.tenantNetworkId] = tenantNetworkAliases[rowHash.tenantNetworkId] || [];
          tenantNetworkAliases[rowHash.tenantNetworkId].push(rowHash.tenantAlias);
        });

      return callback(null, tenantNetworkAliases);
    }
  );
};

/**
 * Convert a Cassandra row into a tenant network.
 *
 * @param  {Row}            row     A Cassandra row that was queried from TenantNetwork
 * @return {TenantNetwork}          The tenant network that is represented by the row of data
 * @api private
 */
const _rowToTenantNetwork = function (row) {
  row = rowToHash(row);
  if (!row.displayName) {
    return null;
  }

  return new TenantNetwork(row.id, row.displayName);
};

/**
 * Ensure the tenant networks caches are populated.
 *
 * @param  {Function}   callback        Standard callback function
 * @param  {Object}     callback.err    An error that occurred, if any
 * @api private
 */
const _ensureCache = function (callback) {
  if (_cacheTenantNetworks) {
    return callback();
  }

  // Load all known tenant networks from Cassandra
  _getAllTenantNetworksFromCassandra((error, tenantNetworks) => {
    if (error) {
      return callback(error);
    }

    // Load all known tenant network tenant associations from Cassandra
    _getAllTenantNetworkTenantAliasesFromCassandra(_.keys(tenantNetworks), (error, tenantNetworkTenantAliases) => {
      if (error) {
        return callback(error);
      }

      // Reset the caches
      _cacheTenantNetworks = tenantNetworks;
      _cacheTenantAliasesByTenantNetworkId = tenantNetworkTenantAliases;
      _cacheTenantNetworkIdsByTenantAlias = {};

      // Build the inverted TenantAlias->TenantNetworkIds cache
      _.each(_cacheTenantAliasesByTenantNetworkId, (tenantAliases, tenantNetworkId) => {
        _.each(tenantAliases, (tenantAlias) => {
          _cacheTenantNetworkIdsByTenantAlias[tenantAlias] = _cacheTenantNetworkIdsByTenantAlias[tenantAlias] || [];
          _cacheTenantNetworkIdsByTenantAlias[tenantAlias].push(tenantNetworkId);
        });
      });

      emitter.emit('revalidate');
      return callback();
    });
  });
};

/**
 * Invalidate all tenant network caches of all nodes in the cluster. The cache of this node will be cleared
 * by the end of this invocation.
 *
 * @api private
 */
const _invalidateAllCaches = function () {
  _invalidateLocalCache();
  Pubsub.publish('oae-tenant-networks', 'invalidate');
};

/**
 * Invalidate all local tenant network caches so that they may be repopulated at the next request.
 *
 * @api private
 */
const _invalidateLocalCache = function () {
  _cacheTenantNetworks = null;
  _cacheTenantAliasesByTenantNetworkId = null;
  _cacheTenantNetworkIdsByTenantAlias = null;
  emitter.emit('invalidate');
};

export {
  emitter,
  init,
  createTenantNetwork,
  getAllTenantNetworks,
  getTenantNetwork,
  updateTenantNetwork,
  deleteTenantNetwork,
  addTenantAliases,
  getAllTenantNetworkTenantAliases,
  removeTenantAliases
};