request.js

const _ = require('underscore');
const md5 = require('md5');
const { extend } = require('underscore');
const AV = require('./av');
const { getSessionToken } = require('./utils');
const ajax = require('./utils/ajax');

// 计算 X-LC-Sign 的签名方法
const sign = (key, isMasterKey) => {
  const now = new Date().getTime();
  const signature = md5(now + key);
  if (isMasterKey) {
    return `${signature},${now},master`;
  }
  return `${signature},${now}`;
};

const setAppKey = (headers, signKey) => {
  if (signKey) {
    headers['X-LC-Sign'] = sign(AV.applicationKey);
  } else {
    headers['X-LC-Key'] = AV.applicationKey;
  }
};

const setHeaders = (authOptions = {}, signKey) => {
  const headers = {
    'X-LC-Id': AV.applicationId,
    'Content-Type': 'application/json;charset=UTF-8',
  };
  let useMasterKey = false;
  if (typeof authOptions.useMasterKey === 'boolean') {
    useMasterKey = authOptions.useMasterKey;
  } else if (typeof AV._config.useMasterKey === 'boolean') {
    useMasterKey = AV._config.useMasterKey;
  }
  if (useMasterKey) {
    if (AV.masterKey) {
      if (signKey) {
        headers['X-LC-Sign'] = sign(AV.masterKey, true);
      } else {
        headers['X-LC-Key'] = `${AV.masterKey},master`;
      }
    } else {
      console.warn('masterKey is not set, fall back to use appKey');
      setAppKey(headers, signKey);
    }
  } else {
    setAppKey(headers, signKey);
  }
  if (AV.hookKey) {
    headers['X-LC-Hook-Key'] = AV.hookKey;
  }
  if (AV._config.production !== null) {
    headers['X-LC-Prod'] = String(AV._config.production);
  }
  headers[process.env.PLATFORM === 'NODE_JS' ? 'User-Agent' : 'X-LC-UA'] =
    AV._sharedConfig.userAgent;

  return Promise.resolve().then(() => {
    // Pass the session token
    const sessionToken = getSessionToken(authOptions);
    if (sessionToken) {
      headers['X-LC-Session'] = sessionToken;
    } else if (!AV._config.disableCurrentUser) {
      return AV.User.currentAsync().then(currentUser => {
        if (currentUser && currentUser._sessionToken) {
          headers['X-LC-Session'] = currentUser._sessionToken;
        }
        return headers;
      });
    }
    return headers;
  });
};

const createApiUrl = ({
  service = 'api',
  version = '1.1',
  path,
  // query, // don't care
  // method, // don't care
  // data, // don't care
}) => {
  let apiURL = AV._config.serverURLs[service];

  if (!apiURL) throw new Error(`undefined server URL for ${service}`);

  if (apiURL.charAt(apiURL.length - 1) !== '/') {
    apiURL += '/';
  }
  apiURL += version;
  if (path) {
    apiURL += path;
  }

  return apiURL;
};

/**
 * Low level REST API client. Call REST endpoints with authorization headers.
 * @function AV.request
 * @since 3.0.0
 * @param {Object} options
 * @param {String} options.method HTTP method
 * @param {String} options.path endpoint path, e.g. `/classes/Test/55759577e4b029ae6015ac20`
 * @param {Object} [options.query] query string dict
 * @param {Object} [options.data] HTTP body
 * @param {AuthOptions} [options.authOptions]
 * @param {String} [options.service = 'api']
 * @param {String} [options.version = '1.1']
 */
const request = ({
  service,
  version,
  method,
  path,
  query,
  data,
  authOptions,
  signKey = true,
}) => {
  if (!(AV.applicationId && (AV.applicationKey || AV.masterKey))) {
    throw new Error('Not initialized');
  }
  if (AV._appRouter) {
    AV._appRouter.refresh();
  }
  const { requestTimeout: timeout } = AV._config;
  const url = createApiUrl({ service, path, version });
  return setHeaders(authOptions, signKey).then(headers =>
    ajax({ method, url, query, data, headers, timeout }).catch(error => {
      let errorJSON = {
        code: error.code || -1,
        error: error.message || error.responseText,
      };
      if (error.response && error.response.code) {
        errorJSON = error.response;
      } else if (error.responseText) {
        try {
          errorJSON = JSON.parse(error.responseText);
        } catch (e) {
          // If we fail to parse the error text, that's okay.
        }
      }
      errorJSON.rawMessage = errorJSON.rawMessage || errorJSON.error;
      if (!AV._sharedConfig.keepErrorRawMessage) {
        errorJSON.error += ` [${error.statusCode || 'N/A'} ${method} ${url}]`;
      }
      // Transform the error into an instance of AVError by trying to parse
      // the error string as JSON.
      const err = new Error(errorJSON.error);
      delete errorJSON.error;
      throw _.extend(err, errorJSON);
    })
  );
};

// lagecy request
const _request = (
  route,
  className,
  objectId,
  method,
  data,
  authOptions,
  query
) => {
  let path = '';
  if (route) path += `/${route}`;
  if (className) path += `/${className}`;
  if (objectId) path += `/${objectId}`;
  // for migeration
  if (data && data._fetchWhenSave)
    throw new Error('_fetchWhenSave should be in the query');
  if (data && data._where) throw new Error('_where should be in the query');
  if (method && method.toLowerCase() === 'get') {
    query = extend({}, query, data);
    data = null;
  }
  return request({
    method,
    path,
    query,
    data,
    authOptions,
  });
};

AV.request = request;

module.exports = {
  _request,
  request,
};