Ctrl K
ComunidadGuías…

server/api/DataRequestRoute.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const { withSourceExecutionContext } = require("../modules/sourceExecution");
const rateLimit = require("express-rate-limit");

const DataRequestController = require("../controllers/DataRequestController");
const TeamController = require("../controllers/TeamController");
const verifyToken = require("../modules/verifyToken");
const DatasetController = require("../controllers/DatasetController");
const ConnectionController = require("../controllers/ConnectionController");
const { getDataRecovery } = require("../modules/dataRecovery");

const apiLimiter = (max = 10) => {
  return rateLimit({
    windowMs: 60 * 1000, // 1 minute
    max,
  });
};

module.exports = (app) => {
  const dataRequestController = new DataRequestController();
  const teamController = new TeamController();
  const datasetController = new DatasetController();
  const connectionController = new ConnectionController();

  const root = "/team/:team_id/datasets/:dataset_id/dataRequests";
  const hasProjectAccess = (projectIds = [], projects = []) => {
    if (!Array.isArray(projectIds) || !Array.isArray(projects)) return false;
    return projectIds.some((projectId) => projects.includes(projectId));
  };

  const checkPermissions = async (req, res, next) => {
    const { team_id } = req.params;

    // Fetch the TeamRole for the user
    const teamRole = await teamController.getTeamRole(team_id, req.user.id);

    if (!teamRole) {
      return res.status(403).json({ message: "Access denied" });
    }

    let dataset;
    try {
      dataset = await datasetController.findByIdAndTeam(req.params.dataset_id, team_id);
    } catch (error) {
      if (error.message === "404") return res.status(403).json({ message: "Access denied" });
      throw error;
    }

    const { role, projects } = teamRole;
    let currentDataRequest;

    // Handle permissions for teamOwner and teamAdmin
    if (["teamOwner", "teamAdmin"].includes(role)) {
      req.user.canAccessSensitiveConnectionMetadata = true;

      if (req.params.id) {
        currentDataRequest = await dataRequestController.findById(req.params.id);
        if (`${currentDataRequest.dataset_id}` !== `${req.params.dataset_id}`) {
          return res.status(403).json({ message: "Access denied" });
        }
      }

      if (req?.body?.connection_id) {
        const connection = await connectionController.findById(req.body.connection_id);
        if (connection.team_id !== teamRole?.team_id) {
          return res.status(403).json({ message: "Access denied" });
        }
      }

      if (req?.body?.dataset_id) {
        const dataset = await datasetController.findById(req.body.dataset_id);
        if (dataset.team_id !== teamRole?.team_id) {
          return res.status(403).json({ message: "Access denied" });
        }
      }

      return next();
    }

    if (role === "projectAdmin") {
      req.user.canAccessSensitiveConnectionMetadata = false;

      if (!hasProjectAccess(dataset?.project_ids, projects)) {
        return res.status(403).json({ message: "Access denied" });
      }

      if (req.params.id) {
        currentDataRequest = await dataRequestController.findById(req.params.id);
        if (`${currentDataRequest.dataset_id}` !== `${req.params.dataset_id}`) {
          return res.status(403).json({ message: "Access denied" });
        }
      }

      if (req?.body?.connection_id) {
        const connectionChanged = !currentDataRequest || `${currentDataRequest.connection_id}` !== `${req.body.connection_id}`;

        if (connectionChanged) {
          const connection = await connectionController.findById(req.body.connection_id);
          if (`${connection.team_id}` !== `${team_id}` || !hasProjectAccess(connection?.project_ids, projects)) {
            return res.status(403).json({ message: "Access denied" });
          }
        }
      }

      return next();
    }

    return res.status(403).json({ message: "Access denied" });
  };

  /*
  ** Route to create a new Data request
  */
  app.post(`${root}`, verifyToken, checkPermissions, (req, res) => {
    req.body.dataset_id = parseInt(req.params.dataset_id, 10);
    return dataRequestController.create(req.body)
      .then((dataRequest) => {
        return res.status(200).send(dataRequest);
      })
      .catch((error) => {
        if (error.message === "401") {
          return res.status(401).send({ error: "Not authorized" });
        }

        return res.status(400).send(error);
      });
  });
  // -------------------------------------------------

  /*
  ** Route to get a Data Request by dataset ID
  */
  app.get(`${root}`, verifyToken, checkPermissions, (req, res) => {
    return dataRequestController.findByDataset(req.params.dataset_id)
      .then((dataRequests) => {
        return res.status(200).send(dataRequests);
      })
      .catch((error) => {
        if (error && error.message === "404") {
          return res.status(404).send(error);
        }

        return res.status(400).send(error);
      });
  });
  // -------------------------------------------------

  /*
  ** Route to get Data request by ID
  */
  app.get(`${root}/:id`, verifyToken, checkPermissions, (req, res) => {
    return dataRequestController.findById()
      .then((dataRequest) => {
        return res.status(200).send(dataRequest);
      })
      .catch((error) => {
        if (error.message === "401") {
          return res.status(401).send({ error: "Not authorized" });
        }

        return res.status(400).send(error);
      });
  });
  // -------------------------------------------------

  /*
  ** Route to get builder metadata for a data request without exposing connection secrets
  */
  app.get(`${root}/:id/builder-metadata`, verifyToken, checkPermissions, (req, res) => {
    return dataRequestController.getBuilderMetadata(req.params.id, {
      propertyId: req.query.property_id,
      includeSensitive: !!req.user.canAccessSensitiveConnectionMetadata,
    })
      .then((metadata) => {
        return res.status(200).send(metadata);
      })
      .catch((error) => {
        if (error && error.message === "404") {
          return res.status(404).send(error);
        }

        return res.status(400).send(error?.message || error);
      });
  });
  // -------------------------------------------------

  /*
  ** Route to update the dataRequest
  */
  app.put(`${root}/:id`, verifyToken, checkPermissions, (req, res) => {
    req.body.dataset_id = parseInt(req.params.dataset_id, 10);
    return dataRequestController.update(req.params.id, req.body)
      .then((dataRequest) => {
        return res.status(200).send(dataRequest);
      })
      .catch((error) => {
        if (error.message === "401") {
          return res.status(401).send({ error: "Not authorized" });
        }

        return res.status(400).send(error);
      });
  });
  // -------------------------------------------------

  /*
  ** Route to delete a Data Request by ID
  */
  app.delete(`${root}/:id`, verifyToken, checkPermissions, (req, res) => {
    return dataRequestController.delete(req.params.id)
      .then((dataRequest) => {
        return res.status(200).send(dataRequest);
      })
      .catch((error) => {
        if (error.message === "401") {
          return res.status(401).send({ error: "Not authorized" });
        }

        return res.status(400).send(error);
      });
  });
  // -------------------------------------------------

  /*
  ** Route to run a request
  */
  app.post(`${root}/:id/request`, verifyToken, checkPermissions, (req, _res, next) => {
    return withSourceExecutionContext({ activity: "preview", teamId: req.params.team_id }, next);
  }, (req, res) => {
    return dataRequestController.runRequest({
      id: req.params.id,
      chart_id: req.params.chart_id,
      noSource: req.body.noSource,
      getCache: req.body.getCache,
      filters: req.body.filters,
      variables: req.body.variables,
    })
      .then((dataRequest) => {
        const newDataRequest = dataRequest;
        // reduce the size of the returned data. No point in showing thousands of objects
        if (newDataRequest?.dataRequest?.responseData?.data) {
          const { data } = newDataRequest.dataRequest.responseData;
          if (typeof data === "object" && data instanceof Array) {
            newDataRequest.dataRequest.responseData.data = data.slice(0, 20);
          } else if (typeof data === "object") {
            const resultsKey = [];
            Object.keys(data).forEach((key) => {
              if (data[key] instanceof Array) {
                resultsKey.push(key);
              }
            });

            if (resultsKey.length > 0) {
              resultsKey.forEach((resultKey) => {
                const slicedArray = data[resultKey].slice(0, 20);
                newDataRequest.dataRequest.responseData.data[resultKey] = slicedArray;
              });
            }
          }
        }
        return res.status(200).send(newDataRequest);
      })
      .catch((error) => {
        if (error.message === "401") {
          return res.status(401).send({ error: "Not authorized" });
        }

        const recovery = getDataRecovery(error);
        return res.status(400).json({ error: recovery?.message || error.message, ...(recovery ? { recovery } : {}) });
      });
  });
  // -------------------------------------------------

  /*
  ** Route to ask AI a question
  */
  app.post(`${root}/:id/askAi`, verifyToken, checkPermissions, apiLimiter(10), (req, res) => {
    return dataRequestController.askAi(
      req.params.id,
      req.body.question,
      req.body.conversationHistory,
      req.body.currentQuery,
    )
      .then((aiResponse) => {
        return res.status(200).send(aiResponse);
      })
      .catch((error) => {
        return res.status(400).send(error);
      });
  });
  // -------------------------------------------------

  /*
  ** Route to propose a source-owned DataRequest configuration
  */
  app.post(`${root}/:id/ai-configuration`, verifyToken, checkPermissions, apiLimiter(10), (req, res) => {
    return dataRequestController.proposeAiConfiguration(
      req.params.id,
      req.body.question,
      req.body.currentConfiguration,
    )
      .then((proposal) => {
        return res.status(200).send(proposal);
      })
      .catch((error) => {
        return res.status(error.statusCode || 400).json({ error: error.message });
      });
  });
  // -------------------------------------------------

  /*
  ** Route to create a new variable binding
  */
  app.post(`${root}/:id/variableBindings`, verifyToken, checkPermissions, (req, res) => {
    return dataRequestController.createVariableBinding(
      req.params.id,
      req.body,
    )
      .then((variableBinding) => {
        return res.status(200).send(variableBinding);
      });
  });
  // -------------------------------------------------

  /*
  ** Route to update a variable binding
  */
  app.put(`${root}/:id/variableBindings/:variable_id`, verifyToken, checkPermissions, (req, res) => {
    return dataRequestController.updateVariableBinding(
      req.params.id,
      req.params.variable_id,
      req.body,
    )
      .then((variableBinding) => {
        return res.status(200).send(variableBinding);
      });
  });
  // -------------------------------------------------

  /*
  ** Route to delete a variable binding
  */
  app.delete(`${root}/:id/variableBindings/:variable_id`, verifyToken, checkPermissions, (req, res) => {
    return dataRequestController.deleteVariableBinding(
      req.params.id,
      req.params.variable_id,
    )
      .then((dataRequest) => {
        return res.status(200).send(dataRequest);
      });
  });
  // -------------------------------------------------

  return (req, res, next) => {
    next();
  };
};