<commit_before>import r2 from 'r2';
import sizeOf from 'image-size';
import request from 'request';
import { queryString, getCloudinaryUrl, svg2png } from '../lib/utils';
import { fetchMembersStats, fetchMembers } from '../lib/graphql';
import cachedRequestLib from 'cached-request';
import Promise from 'bluebird';
import url from 'url';

const cachedRequest = cachedRequestLib(request);
cachedRequest.setCacheDirectory('/tmp');
const requestPromise = Promise.promisify(cachedRequest, { multiArgs: true });

// Cache the list of members of a collective to avoid requesting it for every single /:collectiveSlug/backers/:position/avatar
const cache = require('lru-cache')({
  max: 1000,
  maxAge: 1000 * 60 * 10
});

const WEBSITE_URL = process.env.WEBSITE_URL || "https://opencollective.com";

/**
 * Generates a github badge for a backerType (backers|sponsors) or for a tierSlug
 */
export async function badge(req, res) {
  try {
    const { style } = req.query;
    const color = req.query.color || 'brightgreen';

    const stats = await fetchMembersStats(req.params);

    const filename = `${stats.name}-${stats.count? stats.count : 0}-${color}.svg`;
    const imageUrl = `https://img.shields.io/badge/${filename}?style=${style}`;

    try {
      const imageRequest = await r2(imageUrl).text;
      res.setHeader('content-type','image/svg+xml;charset=utf-8');
      res.setHeader('cache-control','max-age=600');
      return res.send(imageRequest);
    } catch (e) {
      console.error(">>> error while fetching", imageUrl, e);
      res.setHeader('cache-control','max-age=30');
      return res.status(500).send(`Unable to fetch ${imageUrl}`);
    }
  } catch (e) {
    console.error("Catching an error in controllers.collectives.badge", e);
  }
}

export async function banner(req, res) {
  const { collectiveSlug, tierSlug, backerType } = req.params;
  const format = req.params.format || 'svg';
  const style = req.query.style || 'rounded';
  const limit = Number(req.query.limit) || Infinity;
  const imageWidth = Number(req.query.width) || 0;
  const imageHeight = Number(req.query.height) || 0;
  const showBtn = (req.query.button === 'false') ? false : true;

  let users = cache.get(queryString.stringify(req.params));
  if (!users) {
    users = await fetchMembers(req.params);
    cache.set(queryString.stringify(req.params), users);
  }

  const count = Math.min(limit, users.length);

  let defaultAvatarHeight = 64;
  let defaultMargin = 5;
  if ( users.length > 50) {
    defaultAvatarHeight = 48;
    defaultMargin = 3;
  }
  if ( users.length > 150) {
    defaultAvatarHeight = 24;
    defaultMargin = 2;
  }

  const avatarHeight = Number(req.query.avatarHeight) || defaultAvatarHeight;
  const margin = Number(req.query.margin) || defaultMargin;

  const params = (style === 'rounded') ? { query: `/c_thumb,g_face,h_${avatarHeight*2},r_max,w_${avatarHeight*2},bo_3px_solid_white/c_thumb,h_${avatarHeight*2},r_max,w_${avatarHeight*2},bo_2px_solid_rgb:66C71A/e_trim/f_auto/` } : { width: avatarHeight * 2, height: avatarHeight * 2};

  const promises = [];
  for (let i = 0 ; i < count ; i++) {
    let image = users[i].image;
    if (image) {
      if (users[i].type === 'USER') {
        image = getCloudinaryUrl(image, params);
      }
      const options = {url: image, encoding: null, ttl: 60 * 60 * 24 * 30 * 1000}; // 30 days caching
      promises.push(requestPromise(options));
    } else {
      promises.push(Promise.resolve());
    }
  }

  const selector = tierSlug || backerType;
  if (showBtn && selector.length > 0) {
    const btnImage = (selector.match(/sponsor/)) ? 'sponsor' : 'backer';
    const btn = {
      url: `${WEBSITE_URL}/public/images/become_${btnImage}.svg`,
      encoding: null,
      ttl: 60 * 60 * 24 * 30 * 1000
    };

    users.push({
      slug: collectiveSlug,
      website: `${WEBSITE_URL}/${collectiveSlug}#support`
    })

    promises.push(requestPromise(btn));
  }

  let posX = margin;
  let posY = margin;

  Promise.all(promises)
  .then(responses => {
    const images = [];
    for (let i=0;i<responses.length;i++) {
      if (!responses[i]) continue;
      const { headers } = responses[i][0];
      const rawData = responses[i][1];
      const user = users[i];
      if (!user) continue;

      const contentType = headers['content-type'];
      const website = (user.website && (selector === 'contributors' || selector == 'sponsors')) ? user.website : `${WEBSITE_URL}/${user.slug}`;
      const base64data = new Buffer(rawData).toString('base64');
      let avatarWidth = avatarHeight;
      try {
        // We make sure the image loaded properly
        const dimensions = sizeOf(rawData);
        avatarWidth = Math.round(dimensions.width / dimensions.height * avatarHeight);
      } catch (e) {
        // Otherwise, we skip it
        console.error(`Cannot get the dimensions of the avatar of ${user.slug}`, user.image);
        continue;
      }

      if (imageWidth > 0 && posX + avatarWidth + margin > imageWidth) {
        posY += (avatarHeight + margin);
        posX = margin;
      }
      const image = `<image x="${posX}" y="${posY}" width="${avatarWidth}" height="${avatarHeight}" xlink:href="data:${contentType};base64,${base64data}"/>`;
      const imageLink = `<a xlink:href="${website.replace(/&/g,'&')}" target="_blank" id="${user.slug}">${image}</a>`;
      images.push(imageLink);
      posX += avatarWidth + margin;
    }

    return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${imageWidth || posX}" height="${imageHeight || posY + avatarHeight + margin}">
      ${images.join('\n')}
    </svg>`;
  })
  .then(svg => {
    switch (format) {
      case 'svg':
        res.setHeader('content-type','image/svg+xml;charset=utf-8');
        return svg;

      case 'png':
        res.setHeader('content-type','image/png');
        return svg2png(svg)
    }
  })
  .then(svg => {
    res.setHeader('Cache-Control', 'public, max-age=300');
    res.send(svg);
  })
  .catch(e => {
    console.log("Error in generating banner: ", e);
  });
}

export async function website(req, res) {
  const { collectiveSlug, tierSlug, backerType } = req.params;

  let users = cache.get(queryString.stringify({ collectiveSlug, tierSlug, backerType }));
  if (!users) {
    users = await fetchMembers(req.params);
    cache.set(queryString.stringify({ collectiveSlug, tierSlug, backerType }), users);
  }

  const position = parseInt(req.params.position, 10);

  if (position > users.length) {
    return res.sendStatus(404);
  }

  const user = users[position] || {};
  const selector = tierSlug || backerType;
  let redirectUrl = `${WEBSITE_URL}/${user.slug}`;
  if (selector.match(/sponsor/)) {
    user.twitter = user.twitterHandle ? `https://twitter.com/${user.twitterHandle}` : null;
    redirectUrl =  user.website || user.twitter || `${WEBSITE_URL}/${user.slug}`;
  }

  if (position === users.length) {
    redirectUrl = `${WEBSITE_URL}/${collectiveSlug}#support`;
  }

  const parsedUrl = url.parse(redirectUrl);
  const params = queryString.parse(parsedUrl.query);

  params.utm_source = params.utm_source || 'opencollective';
  params.utm_medium = params.utm_medium || 'github';
  params.utm_campaign = params.utm_campaign || collectiveSlug;

  parsedUrl.search = `?${queryString.stringify(params)}`;
  redirectUrl = url.format(parsedUrl);

  req.ga.event(`GithubWidget-${selector}`, `Click`, user.slug, position);

  res.redirect(301, redirectUrl);

}

export async function avatar(req, res) {
  const { collectiveSlug, tierSlug, backerType } = req.params;

  let users = cache.get(queryString.stringify({ collectiveSlug, tierSlug, backerType }));
  if (!users) {
    users = await fetchMembers(req.params);
    cache.set(queryString.stringify({ collectiveSlug, tierSlug, backerType }), users);
  }

  const position = parseInt(req.params.position, 10);
  const user = (position < users.length) ?  users[position] : {};

  const format = req.params.format || 'svg';
  let maxHeight;
  const selector = tierSlug || backerType;
  if (req.query.avatarHeight) {
    maxHeight = Number(req.query.avatarHeight);
  } else {
    maxHeight = (format === 'svg' ) ? 128 : 64;
    if (selector.match(/silver/)) maxHeight *= 1.25;
    if (selector.match(/gold/)) maxHeight *= 1.5;
    if (selector.match(/diamond/)) maxHeight *= 2;
  }

  // We only record a page view when loading the first avatar
  if (position==0) {
    req.ga.pageview();
  }

  let imageUrl = "/public/images/user.svg";
  if (user.image && user.image.substr(0,1) !== '/') {
    if (user.type === 'USER') {
      imageUrl = getCloudinaryUrl(user.image, { query: `/c_thumb,g_face,h_${maxHeight},r_max,w_${maxHeight},bo_3px_solid_white/c_thumb,h_${maxHeight},r_max,w_${maxHeight},bo_2px_solid_rgb:66C71A/e_trim/f_auto/` });
    } else {
      imageUrl = getCloudinaryUrl(user.image, { height: maxHeight });
    }
  }

  if (position == users.length) {
    const btnImage = (selector.match(/sponsor/)) ? 'sponsor' : 'backer';
    imageUrl = `/public/images/become_${btnImage}.svg`;
  } else if (position > users.length) {
    imageUrl = "/public/images/1px.png";
  }

  if (imageUrl.substr(0,1) === '/') {
    return res.redirect(imageUrl);
  }

  if (format === 'svg') {
    request({url: imageUrl, encoding: null}, (err, r, data) => {
      if (err) {
        return res.status(500).send(`Unable to fetch ${imageUrl}`);
      }
      const contentType = r.headers['content-type'];

      const imageHeight = Math.round(maxHeight / 2);
      let imageWidth = 64;
      if (selector.match(/sponsor/)) {
        try {
          const dimensions = sizeOf(data);
          imageWidth = Math.round(dimensions.width / dimensions.height * imageHeight);
        } catch (e) {
          console.error("Unable to get image dimensions for ", imageUrl);
          return res.status(500).send(`Unable to fetch ${imageUrl}`);
        }
      }

      const base64data = new Buffer(data).toString('base64');
      const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${imageWidth}" height="${imageHeight}">
        <image width="${imageWidth}" height="${imageHeight}" xlink:href="data:${contentType};base64,${base64data}"/>
      </svg>`;
      res.setHeader('Cache-Control', 'public, max-age=300');
      res.setHeader('content-type','image/svg+xml;charset=utf-8');
      return res.send(svg);
    });
  } else {
    req
      .pipe(request(imageUrl))
      .on('error', (e) => {
        console.error("error proxying ", imageUrl, e);
        res.status(500).send(e);
      })
      .on('response', (res) => {
        res.headers['Cache-Control'] = 'public, max-age=300';
      })
      .pipe(res);
  }
}<commit_msg>increase cache size by 5x<commit_after>import r2 from 'r2';
import sizeOf from 'image-size';
import request from 'request';
import { queryString, getCloudinaryUrl, svg2png } from '../lib/utils';
import { fetchMembersStats, fetchMembers } from '../lib/graphql';
import cachedRequestLib from 'cached-request';
import Promise from 'bluebird';
import url from 'url';

const cachedRequest = cachedRequestLib(request);
cachedRequest.setCacheDirectory('/tmp');
const requestPromise = Promise.promisify(cachedRequest, { multiArgs: true });

// Cache the list of members of a collective to avoid requesting it for every single /:collectiveSlug/backers/:position/avatar
const cache = require('lru-cache')({
  max: 5000,
  maxAge: 1000 * 60 * 10
});

const WEBSITE_URL = process.env.WEBSITE_URL || "https://opencollective.com";

/**
 * Generates a github badge for a backerType (backers|sponsors) or for a tierSlug
 */
export async function badge(req, res) {
  try {
    const { style } = req.query;
    const color = req.query.color || 'brightgreen';

    const stats = await fetchMembersStats(req.params);

    const filename = `${stats.name}-${stats.count? stats.count : 0}-${color}.svg`;
    const imageUrl = `https://img.shields.io/badge/${filename}?style=${style}`;

    try {
      const imageRequest = await r2(imageUrl).text;
      res.setHeader('content-type','image/svg+xml;charset=utf-8');
      res.setHeader('cache-control','max-age=600');
      return res.send(imageRequest);
    } catch (e) {
      console.error(">>> error while fetching", imageUrl, e);
      res.setHeader('cache-control','max-age=30');
      return res.status(500).send(`Unable to fetch ${imageUrl}`);
    }
  } catch (e) {
    console.error("Catching an error in controllers.collectives.badge", e);
  }
}

export async function banner(req, res) {
  const { collectiveSlug, tierSlug, backerType } = req.params;
  const format = req.params.format || 'svg';
  const style = req.query.style || 'rounded';
  const limit = Number(req.query.limit) || Infinity;
  const imageWidth = Number(req.query.width) || 0;
  const imageHeight = Number(req.query.height) || 0;
  const showBtn = (req.query.button === 'false') ? false : true;

  let users = cache.get(queryString.stringify(req.params));
  if (!users) {
    users = await fetchMembers(req.params);
    cache.set(queryString.stringify(req.params), users);
  }

  const count = Math.min(limit, users.length);

  let defaultAvatarHeight = 64;
  let defaultMargin = 5;
  if ( users.length > 50) {
    defaultAvatarHeight = 48;
    defaultMargin = 3;
  }
  if ( users.length > 150) {
    defaultAvatarHeight = 24;
    defaultMargin = 2;
  }

  const avatarHeight = Number(req.query.avatarHeight) || defaultAvatarHeight;
  const margin = Number(req.query.margin) || defaultMargin;

  const params = (style === 'rounded') ? { query: `/c_thumb,g_face,h_${avatarHeight*2},r_max,w_${avatarHeight*2},bo_3px_solid_white/c_thumb,h_${avatarHeight*2},r_max,w_${avatarHeight*2},bo_2px_solid_rgb:66C71A/e_trim/f_auto/` } : { width: avatarHeight * 2, height: avatarHeight * 2};

  const promises = [];
  for (let i = 0 ; i < count ; i++) {
    let image = users[i].image;
    if (image) {
      if (users[i].type === 'USER') {
        image = getCloudinaryUrl(image, params);
      }
      const options = {url: image, encoding: null, ttl: 60 * 60 * 24 * 30 * 1000}; // 30 days caching
      promises.push(requestPromise(options));
    } else {
      promises.push(Promise.resolve());
    }
  }

  const selector = tierSlug || backerType;
  if (showBtn && selector.length > 0) {
    const btnImage = (selector.match(/sponsor/)) ? 'sponsor' : 'backer';
    const btn = {
      url: `${WEBSITE_URL}/public/images/become_${btnImage}.svg`,
      encoding: null,
      ttl: 60 * 60 * 24 * 30 * 1000
    };

    users.push({
      slug: collectiveSlug,
      website: `${WEBSITE_URL}/${collectiveSlug}#support`
    })

    promises.push(requestPromise(btn));
  }

  let posX = margin;
  let posY = margin;

  Promise.all(promises)
  .then(responses => {
    const images = [];
    for (let i=0;i<responses.length;i++) {
      if (!responses[i]) continue;
      const { headers } = responses[i][0];
      const rawData = responses[i][1];
      const user = users[i];
      if (!user) continue;

      const contentType = headers['content-type'];
      const website = (user.website && (selector === 'contributors' || selector == 'sponsors')) ? user.website : `${WEBSITE_URL}/${user.slug}`;
      const base64data = new Buffer(rawData).toString('base64');
      let avatarWidth = avatarHeight;
      try {
        // We make sure the image loaded properly
        const dimensions = sizeOf(rawData);
        avatarWidth = Math.round(dimensions.width / dimensions.height * avatarHeight);
      } catch (e) {
        // Otherwise, we skip it
        console.error(`Cannot get the dimensions of the avatar of ${user.slug}`, user.image);
        continue;
      }

      if (imageWidth > 0 && posX + avatarWidth + margin > imageWidth) {
        posY += (avatarHeight + margin);
        posX = margin;
      }
      const image = `<image x="${posX}" y="${posY}" width="${avatarWidth}" height="${avatarHeight}" xlink:href="data:${contentType};base64,${base64data}"/>`;
      const imageLink = `<a xlink:href="${website.replace(/&/g,'&')}" target="_blank" id="${user.slug}">${image}</a>`;
      images.push(imageLink);
      posX += avatarWidth + margin;
    }

    return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${imageWidth || posX}" height="${imageHeight || posY + avatarHeight + margin}">
      ${images.join('\n')}
    </svg>`;
  })
  .then(svg => {
    switch (format) {
      case 'svg':
        res.setHeader('content-type','image/svg+xml;charset=utf-8');
        return svg;

      case 'png':
        res.setHeader('content-type','image/png');
        return svg2png(svg)
    }
  })
  .then(svg => {
    res.setHeader('Cache-Control', 'public, max-age=300');
    res.send(svg);
  })
  .catch(e => {
    console.log("Error in generating banner: ", e);
  });
}

export async function website(req, res) {
  const { collectiveSlug, tierSlug, backerType } = req.params;

  let users = cache.get(queryString.stringify({ collectiveSlug, tierSlug, backerType }));
  if (!users) {
    users = await fetchMembers(req.params);
    cache.set(queryString.stringify({ collectiveSlug, tierSlug, backerType }), users);
  }

  const position = parseInt(req.params.position, 10);

  if (position > users.length) {
    return res.sendStatus(404);
  }

  const user = users[position] || {};
  const selector = tierSlug || backerType;
  let redirectUrl = `${WEBSITE_URL}/${user.slug}`;
  if (selector.match(/sponsor/)) {
    user.twitter = user.twitterHandle ? `https://twitter.com/${user.twitterHandle}` : null;
    redirectUrl =  user.website || user.twitter || `${WEBSITE_URL}/${user.slug}`;
  }

  if (position === users.length) {
    redirectUrl = `${WEBSITE_URL}/${collectiveSlug}#support`;
  }

  const parsedUrl = url.parse(redirectUrl);
  const params = queryString.parse(parsedUrl.query);

  params.utm_source = params.utm_source || 'opencollective';
  params.utm_medium = params.utm_medium || 'github';
  params.utm_campaign = params.utm_campaign || collectiveSlug;

  parsedUrl.search = `?${queryString.stringify(params)}`;
  redirectUrl = url.format(parsedUrl);

  req.ga.event(`GithubWidget-${selector}`, `Click`, user.slug, position);

  res.redirect(301, redirectUrl);

}

export async function avatar(req, res) {
  const { collectiveSlug, tierSlug, backerType } = req.params;

  console.log('>>> cache.itemCount', cache.itemCount);
  let users = cache.get(queryString.stringify({ collectiveSlug, tierSlug, backerType }));
  if (!users) {
    users = await fetchMembers(req.params);
    cache.set(queryString.stringify({ collectiveSlug, tierSlug, backerType }), users);
  }

  const position = parseInt(req.params.position, 10);
  const user = (position < users.length) ?  users[position] : {};

  const format = req.params.format || 'svg';
  let maxHeight;
  const selector = tierSlug || backerType;
  if (req.query.avatarHeight) {
    maxHeight = Number(req.query.avatarHeight);
  } else {
    maxHeight = (format === 'svg' ) ? 128 : 64;
    if (selector.match(/silver/)) maxHeight *= 1.25;
    if (selector.match(/gold/)) maxHeight *= 1.5;
    if (selector.match(/diamond/)) maxHeight *= 2;
  }

  // We only record a page view when loading the first avatar
  if (position==0) {
    req.ga.pageview();
  }

  let imageUrl = "/public/images/user.svg";
  if (user.image && user.image.substr(0,1) !== '/') {
    if (user.type === 'USER') {
      imageUrl = getCloudinaryUrl(user.image, { query: `/c_thumb,g_face,h_${maxHeight},r_max,w_${maxHeight},bo_3px_solid_white/c_thumb,h_${maxHeight},r_max,w_${maxHeight},bo_2px_solid_rgb:66C71A/e_trim/f_auto/` });
    } else {
      imageUrl = getCloudinaryUrl(user.image, { height: maxHeight });
    }
  }

  if (position == users.length) {
    const btnImage = (selector.match(/sponsor/)) ? 'sponsor' : 'backer';
    imageUrl = `/public/images/become_${btnImage}.svg`;
  } else if (position > users.length) {
    imageUrl = "/public/images/1px.png";
  }

  if (imageUrl.substr(0,1) === '/') {
    return res.redirect(imageUrl);
  }

  if (format === 'svg') {
    request({url: imageUrl, encoding: null}, (err, r, data) => {
      if (err) {
        return res.status(500).send(`Unable to fetch ${imageUrl}`);
      }
      const contentType = r.headers['content-type'];

      const imageHeight = Math.round(maxHeight / 2);
      let imageWidth = 64;
      if (selector.match(/sponsor/)) {
        try {
          const dimensions = sizeOf(data);
          imageWidth = Math.round(dimensions.width / dimensions.height * imageHeight);
        } catch (e) {
          console.error("Unable to get image dimensions for ", imageUrl);
          return res.status(500).send(`Unable to fetch ${imageUrl}`);
        }
      }

      const base64data = new Buffer(data).toString('base64');
      const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${imageWidth}" height="${imageHeight}">
        <image width="${imageWidth}" height="${imageHeight}" xlink:href="data:${contentType};base64,${base64data}"/>
      </svg>`;
      res.setHeader('Cache-Control', 'public, max-age=300');
      res.setHeader('content-type','image/svg+xml;charset=utf-8');
      return res.send(svg);
    });
  } else {
    req
      .pipe(request(imageUrl))
      .on('error', (e) => {
        console.error("error proxying ", imageUrl, e);
        res.status(500).send(e);
      })
      .on('response', (res) => {
        res.headers['Cache-Control'] = 'public, max-age=300';
      })
      .pipe(res);
  }
}<|endoftext|>


<commit_before>(function () {
    // TODO: Cover this code with tests.
    var capturedClient = buster.create(buster.server.capturedClient);
    capturedClient.listen();
}());
<commit_msg>Updating frame.js to latest changes.<commit_after>(function () {
    // TODO: Cover this code with tests.
    var controlFrame = buster.create(buster.server.controlFrame);
    controlFrame.listen();
}());
<|endoftext|>


<commit_before>"use strict";

var config = require("../shared/config.js"),
    domAdapter = require("../client/domAdapter.js"),
    request = domAdapter.request;

function getRequestUrl(url, parentIds){
    if(url === undefined) {
        throw new Error("(alamid) No Url passed");
    }

    var resUrl = config.baseURL + '/services/';

    //nothing to merge here
    if(parentIds === null) {
        return resUrl + url;
    }

    var urlSplit = url.split("/"),
        resArray = [];

    for(var i = 0; i < urlSplit.length; i++) {
        resArray.push(urlSplit[i]);
        //is an id defined?
        if(parentIds[urlSplit[i]] !== undefined) {
            resArray.push(parentIds[urlSplit[i]]);
        }
    }
    return resUrl + resArray.join("/");
}

function RemoteService(modelUrl) {

    function onRemoteServiceResponse(callback) {
        return function handleResponse(err, rawResponse) {
            var response;

            if(err) {
                callback(err);
                return;
            }

            try {
                response = domAdapter.JSONparse(rawResponse);
            } catch (err) {
                response = rawResponse;
            }
            callback(response);
        };
    }

    this.create = function(model, callback) {
        var url = getRequestUrl(model.getUrl(), model.getParentIds());
        model = model.get();
        request("put", url, model, onRemoteServiceResponse(callback));
    };

    this.read = function(model, callback) {
        var url = getRequestUrl(model.getUrl(), model.getParentIds());
        model = model.get();
        request("get", url, model, onRemoteServiceResponse(callback));
    };

    this.update = function(model, callback) {
        var url = getRequestUrl(model.getUrl(), model.getParentIds());
        model = model.get();
        request("post", url, model, onRemoteServiceResponse(callback));
    };

    this.delete = function(model, callback) {
        var url = getRequestUrl(model.getUrl(), model.getParentIds());
        model = model.get();
        request("delete", url, model, onRemoteServiceResponse(callback));
    };
}

exports.RemoteService = RemoteService;
<commit_msg>- updated RemoteRequest to abstract any request on the client from transports (http/websockets)<commit_after>"use strict";

var config = require("../shared/config.js"),
    domAdapter = require("../client/domAdapter.js"),
    httpRequest = domAdapter.request;

/**
 * function makes a request using the default transport (websockets/http)
 * use this function to abstract any connection with the server
 * @param {String} method (post/put/delete/get)
 * @param {String} url
 * @param {Object} model
 * @param {Function} callback
 */
function request(method, url, model, callback) {

    function onHttpResponse(err, responseBody) {
        var response;

        if(err) {
            callback(err);
            return;
        }

        try {
            response = domAdapter.JSONparse(rawResponse);
        } catch (err) {
            response = responseBody;
        }

        callback(response);
    }

    if(config.useWebsockets && socket !== undefined) {
        socket.emit(method, url, model, callback);
    }
    else{
        httpRequest(method, url, model, onHttpResponse);
    }
}

/**
 * Helper-function to merge urls with given parent IDs
 * makes /blog/comment with id for blog -> blog/1/comment
 * @param {String} url
 * @param {Object} parentIds
 * @return {String}
 */
function getRequestUrl(url, parentIds){
    if(url === undefined) {
        throw new Error("(alamid) No Url passed");
    }

    var resUrl = config.baseURL + '/services/';

    //nothing to merge here
    if(parentIds === null) {
        return resUrl + url;
    }

    var urlSplit = url.split("/"),
        resArray = [];

    for(var i = 0; i < urlSplit.length; i++) {
        resArray.push(urlSplit[i]);
        //is an id defined?
        if(parentIds[urlSplit[i]] !== undefined) {
            resArray.push(parentIds[urlSplit[i]]);
        }
    }
    return resUrl + resArray.join("/");
}

/**
 * calls a remote service using the default transport (websockets or http)
 * @param modelUrl
 * @constructor
 */
function RemoteService(modelUrl) {
    /**
     * could be used to perform default request
     * @param method
     * @param model
     * @param callback
     */
    function defaultRequest(method, model, callback) {
        var url = getRequestUrl(model.getUrl(), model.getParentIds());
        model = model.get();
        request(method, url, model, callback);
    }

    this.create = function(model, callback) {
        defaultRequest("PUT", model, callback);
    };

    this.read = function(model, callback) {
        defaultRequest("GET", model, callback);
    };

    this.update = function(model, callback) {
        defaultRequest("POST", model, callback);
    };

    this.delete = function(model, callback) {
        defaultRequest("DELETE", model, callback);
    };
}

exports.RemoteService = RemoteService;
exports.request = request;
<|endoftext|>


<commit_before>/* global text */

/**
 * Determine if a given string contains unicode characters, specified in options
 *
 * @method hasUnicode
 * @memberof axe.commons.text
 * @instance
 * @param {String} str string to verify
 * @param {Object} options config containing which unicode character sets to verify
 * @property {Boolean} options.emoji verify emoji unicode
 * @property {Boolean} options.nonBmp verify nonBmp unicode
 * @property {Boolean} options.punctuations verify punctuations unicode
 * @returns {Boolean}
 */
text.hasUnicode = function hasUnicode(str, options) {
	const { emoji, nonBmp, punctuations } = options;
	if (emoji) {
		return axe.imports.emojiRegexText().test(str);
	}
	if (nonBmp) {
		return (
			getUnicodeNonBmpRegExp().test(str) ||
			getSupplementaryPrivateUseRegExp().test(str)
		);
	}
	if (punctuations) {
		return getPunctuationRegExp().test(str);
	}
	return false;
};

/**
 * Remove specified type(s) unicode characters
 *
 * @method removeUnicode
 * @memberof axe.commons.text
 * @instance
 * @param {String} str string to operate on
 * @param {Object} options config containing which unicode character sets to remove
 * @property {Boolean} options.emoji remove emoji unicode
 * @property {Boolean} options.nonBmp remove nonBmp unicode
 * @property {Boolean} options.punctuations remove punctuations unicode
 * @returns {String}
 */
text.removeUnicode = function removeUnicode(str, options) {
	const { emoji, nonBmp, punctuations } = options;

	if (emoji) {
		str = str.replace(axe.imports.emojiRegexText(), '');
	}
	if (nonBmp) {
		str = str.replace(getUnicodeNonBmpRegExp(), '');
		str = str.replace(getSupplementaryPrivateUseRegExp(), '');
	}
	if (punctuations) {
		str = str.replace(getPunctuationRegExp(), '');
	}

	return str;
};

/**
 * Regex for matching unicode values out of Basic Multilingual Plane (BMP)
 * Reference:
 * - https://github.com/mathiasbynens/regenerate
 * - https://unicode-table.com/
 * - https://mathiasbynens.be/notes/javascript-unicode
 *
 * @returns {RegExp}
 */
function getUnicodeNonBmpRegExp() {
	/**
	 * Regex for matching astral plane unicode
	 * - http://kourge.net/projects/regexp-unicode-block
	 */

	/**
	 * Notes on various unicode planes being used in the regex below:
	 * '\u1D00-\u1D7F'  Phonetic Extensions
	 * '\u1D80-\u1DBF'  Phonetic Extensions Supplement
	 * '\u1DC0-\u1DFF'  Combining Diacritical Marks Supplement
	 * '\u20A0-\u20CF'  Currency symbols
	 * '\u20D0-\u20FF'  Combining Diacritical Marks for Symbols
	 * '\u2100-\u214F'  Letter like symbols
	 * '\u2150-\u218F'  Number forms (eg: Roman numbers)
	 * '\u2190-\u21FF'  Arrows
	 * '\u2200-\u22FF'  Mathematical operators
	 * '\u2300-\u23FF'  Misc Technical
	 * '\u2400-\u243F'  Control pictures
	 * '\u2440-\u245F'  OCR
	 * '\u2460-\u24FF'  Enclosed alpha numerics
	 * '\u2500-\u257F'  Box Drawing
	 * '\u2580-\u259F'  Block Elements
	 * '\u25A0-\u25FF'  Geometric Shapes
	 * '\u2600-\u26FF'  Misc Symbols
	 * '\u2700-\u27BF'  Dingbats
	 * '\uE000-\uF8FF'  Private Use
	 *
	 * Note: plane '\u2000-\u206F' used for General punctuation is excluded as it is handled in -> getPunctuationRegExp
	 */

	return /[\u1D00-\u1D7F\u1D80-\u1DBF\u1DC0-\u1DFF\u20A0-\u20CF\u20D0-\u20FF\u2100-\u214F\u2150-\u218F\u2190-\u21FF\u2200-\u22FF\u2300-\u23FF\u2400-\u243F\u2440-\u245F\u2460-\u24FF\u2500-\u257F\u2580-\u259F\u25A0-\u25FF\u2600-\u26FF\u2700-\u27BF\uE000-\uF8FF]/g;
}

/**
 * Get regular expression for matching punctuations
 *
 * @returns {RegExp}
 */
function getPunctuationRegExp() {
	/**
	 * Reference: http://kunststube.net/encoding/
	 * US-ASCII
	 * -> !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
	 *
	 * General Punctuation block
	 * -> \u2000-\u206F
	 *
	 * Supplemental Punctuation block
	 * Reference: https://en.wikipedia.org/wiki/Supplemental_Punctuation
	 * -> \u2E00-\u2E7F Reference
	 */
	return /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/g;
}

/**
 * Get regular expression for supplementary private use
 *
 * @returns {RegExp}
 */
function getSupplementaryPrivateUseRegExp() {
	/**
	 * Reference: https://www.unicode.org/charts/PDF/UD800.pdf
	 * https://www.unicode.org/charts/PDF/UDC00.pdf
	 * https://www.unicode.org/charts/PDF/UF0000.pdf
	 */
	return /[\uDB80-\uDBBF][\uDC00-\uDFFD]|(?:[\uDB80-\uDBBE][\uDC00-\uDFFF]|\uDBBF[\uDC00-\uDFFD])/g;
}
<commit_msg>Document which parts of the `getSupplementaryPrivateUseRegExp` regex refer to which unicode range<commit_after>/* global text */

/**
 * Determine if a given string contains unicode characters, specified in options
 *
 * @method hasUnicode
 * @memberof axe.commons.text
 * @instance
 * @param {String} str string to verify
 * @param {Object} options config containing which unicode character sets to verify
 * @property {Boolean} options.emoji verify emoji unicode
 * @property {Boolean} options.nonBmp verify nonBmp unicode
 * @property {Boolean} options.punctuations verify punctuations unicode
 * @returns {Boolean}
 */
text.hasUnicode = function hasUnicode(str, options) {
	const { emoji, nonBmp, punctuations } = options;
	if (emoji) {
		return axe.imports.emojiRegexText().test(str);
	}
	if (nonBmp) {
		return (
			getUnicodeNonBmpRegExp().test(str) ||
			getSupplementaryPrivateUseRegExp().test(str)
		);
	}
	if (punctuations) {
		return getPunctuationRegExp().test(str);
	}
	return false;
};

/**
 * Remove specified type(s) unicode characters
 *
 * @method removeUnicode
 * @memberof axe.commons.text
 * @instance
 * @param {String} str string to operate on
 * @param {Object} options config containing which unicode character sets to remove
 * @property {Boolean} options.emoji remove emoji unicode
 * @property {Boolean} options.nonBmp remove nonBmp unicode
 * @property {Boolean} options.punctuations remove punctuations unicode
 * @returns {String}
 */
text.removeUnicode = function removeUnicode(str, options) {
	const { emoji, nonBmp, punctuations } = options;

	if (emoji) {
		str = str.replace(axe.imports.emojiRegexText(), '');
	}
	if (nonBmp) {
		str = str.replace(getUnicodeNonBmpRegExp(), '');
		str = str.replace(getSupplementaryPrivateUseRegExp(), '');
	}
	if (punctuations) {
		str = str.replace(getPunctuationRegExp(), '');
	}

	return str;
};

/**
 * Regex for matching unicode values out of Basic Multilingual Plane (BMP)
 * Reference:
 * - https://github.com/mathiasbynens/regenerate
 * - https://unicode-table.com/
 * - https://mathiasbynens.be/notes/javascript-unicode
 *
 * @returns {RegExp}
 */
function getUnicodeNonBmpRegExp() {
	/**
	 * Regex for matching astral plane unicode
	 * - http://kourge.net/projects/regexp-unicode-block
	 */

	/**
	 * Notes on various unicode planes being used in the regex below:
	 * '\u1D00-\u1D7F'  Phonetic Extensions
	 * '\u1D80-\u1DBF'  Phonetic Extensions Supplement
	 * '\u1DC0-\u1DFF'  Combining Diacritical Marks Supplement
	 * '\u20A0-\u20CF'  Currency symbols
	 * '\u20D0-\u20FF'  Combining Diacritical Marks for Symbols
	 * '\u2100-\u214F'  Letter like symbols
	 * '\u2150-\u218F'  Number forms (eg: Roman numbers)
	 * '\u2190-\u21FF'  Arrows
	 * '\u2200-\u22FF'  Mathematical operators
	 * '\u2300-\u23FF'  Misc Technical
	 * '\u2400-\u243F'  Control pictures
	 * '\u2440-\u245F'  OCR
	 * '\u2460-\u24FF'  Enclosed alpha numerics
	 * '\u2500-\u257F'  Box Drawing
	 * '\u2580-\u259F'  Block Elements
	 * '\u25A0-\u25FF'  Geometric Shapes
	 * '\u2600-\u26FF'  Misc Symbols
	 * '\u2700-\u27BF'  Dingbats
	 * '\uE000-\uF8FF'  Private Use
	 *
	 * Note: plane '\u2000-\u206F' used for General punctuation is excluded as it is handled in -> getPunctuationRegExp
	 */

	return /[\u1D00-\u1D7F\u1D80-\u1DBF\u1DC0-\u1DFF\u20A0-\u20CF\u20D0-\u20FF\u2100-\u214F\u2150-\u218F\u2190-\u21FF\u2200-\u22FF\u2300-\u23FF\u2400-\u243F\u2440-\u245F\u2460-\u24FF\u2500-\u257F\u2580-\u259F\u25A0-\u25FF\u2600-\u26FF\u2700-\u27BF\uE000-\uF8FF]/g;
}

/**
 * Get regular expression for matching punctuations
 *
 * @returns {RegExp}
 */
function getPunctuationRegExp() {
	/**
	 * Reference: http://kunststube.net/encoding/
	 * US-ASCII
	 * -> !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
	 *
	 * General Punctuation block
	 * -> \u2000-\u206F
	 *
	 * Supplemental Punctuation block
	 * Reference: https://en.wikipedia.org/wiki/Supplemental_Punctuation
	 * -> \u2E00-\u2E7F Reference
	 */
	return /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/g;
}

/**
 * Get regular expression for supplementary private use
 *
 * @returns {RegExp}
 */
function getSupplementaryPrivateUseRegExp() {
	// 1. High surrogate area (https://www.unicode.org/charts/PDF/UD800.pdf)
	// 2. Low surrogate area (https://www.unicode.org/charts/PDF/UDC00.pdf)
	// 3. Supplementary private use area A (https://www.unicode.org/charts/PDF/UF0000.pdf)
	//
	//             1              2                                  3
	//      ┏━━━━━━┻━━━━━━┓┏━━━━━━┻━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
	return /[\uDB80-\uDBBF][\uDC00-\uDFFD]|(?:[\uDB80-\uDBBE][\uDC00-\uDFFF]|\uDBBF[\uDC00-\uDFFD])/g;
}
<|endoftext|>


<commit_before>//
//  This class wraps composable pieces of logic for manipulating chunk data.
//  To use call one of the static methods to get an instance, and then apply
//  mutations as desired before calling the generate function to get the final
//  chunk description data.
//
//  Example:
//      ChunkBuilder.newEmpty()
//          .fillSquare('grass', {width: 20, height: 20})
//          .addCoast()
//          .padWithWater(10)
//          .generateChunk();
//
const TILE_SIZE = 32;   // TODO: this should be configured with the tile set to be flexible

class ChunkBuilder {
    // Static Initializers
    static newEmpty() {
        return new ChunkBuilder();
    }

    // Modifier API
    fillSquare(tileType, {top = 0, left = 0, width = 1, height = 1} = {}) { // jshint ignore:line
        const totalWidth = left + width;
        const totalHeight = top + height;

        while (this.height < totalHeight) {
            this.addRow();
        }

        while (this.width < totalWidth) {
            this.addColumn();
        }

        for (let x = left; x < totalWidth; x++) {
            for (let y = top; y < totalHeight; y++) {
                this.grid[y][x].tile = tileType;
            }
        }

        return this;
    }

    addSimpleHouse(top = 0, left = 0) {
        const X = x => x + left;
        const Y = y => y + top;
        const grid = (x, y) => this.grid[Y(y)][X(x)];
        const tile = (x, y, tile) => grid(x, y).tile = tile;

        tile(0, 1, 'P03');
        tile(1, 0, 'P03');

        return this;
    }

    addCoast() {
        // TODO: This is very locked into the one tileset, we should generalize this sort of logic at some point

        // Grass to Shallow water transition
        this.addRow(true, 'H23');
        this.addRow(false, 'H25');
        this.addColumn(true,'G24');
        this.addColumn(false, 'I24');
        this.topLeft().tile = 'G23';
        this.bottomLeft().tile = 'G25';
        this.topRight().tile = 'I23';
        this.bottomRight().tile = 'I25';

        // Shallow water to deep water transition
        this.addRow(true, 'H19');
        this.addRow(false, 'H17');
        this.addColumn(true,'I18');
        this.addColumn(false, 'G18');
        this.topLeft().tile = 'C17';
        this.bottomLeft().tile = 'A17';
        this.topRight().tile = 'D17';
        this.bottomRight().tile = 'B17';

        return this;
    }

    padWithWater(paddingDepth = 1) {
        const water = 'F22';

        for (let i = 0; i < paddingDepth; i++) {
            this.addRow(true, water);
            this.addRow(false, water);
            this.addColumn(true, water);
            this.addColumn(false, water);
        }

        return this;
    }

    generateChunk() {
        const tilePositions = [];

        // Grab a top level clone of the grid array and reverse it to match the inverted display coordinates used by the client for rendering.
        const grid = this.grid.slice(0);
        grid.reverse();

        for (let x = 0; x < this.width; x++) {
            for (let y = 0; y < this.height; y++) {
                const cell = grid[y][x];

                if (cell.tile) {
                    tilePositions.push({
                        type: cell.tile,
                        position: {
                            x: x * TILE_SIZE,
                            y: y * TILE_SIZE
                        }
                    });
                }
            }
        }

        return tilePositions;
    }

    // Private helpers
    constructor() {
        this.width = 1;
        this.height = 1;
        this.grid = [[new Cell()]];   // initialize to a 1x1 matrix
    }

    addRow(atTop = false, tile = null) {
        const row = [];
        for (let i = 0; i < this.width; i++) {
            row.push(new Cell(tile));
        }

        if (atTop) {
            this.grid.unshift(row);
        } else {
            this.grid.push(row);
        }

        this.height++;
    }

    addColumn(atFront = false, tile = null) {
        this.grid.forEach(
            atFront ?
                row => row.unshift(new Cell(tile)) :
                row => row.push(new Cell(tile))
        );
        this.width++;
    }

    topLeft() {
        return this.grid[0][0];
    }

    topRight() {
        return this.grid[0][this.width - 1];
    }

    bottomLeft() {
        return this.grid[this.height - 1][0];
    }

    bottomRight() {
        return this.grid[this.height - 1][this.width - 1];
    }
}

class Cell {
    constructor(tile) {
        this.tile = tile;
        this.clutter = null;    // not implemented
        this.passable = true;   // not implemented
    }
}

export default ChunkBuilder;<commit_msg>first two rows and fix indexing bug<commit_after>//
//  This class wraps composable pieces of logic for manipulating chunk data.
//  To use call one of the static methods to get an instance, and then apply
//  mutations as desired before calling the generate function to get the final
//  chunk description data.
//
//  Example:
//      ChunkBuilder.newEmpty()
//          .fillSquare('grass', {width: 20, height: 20})
//          .addCoast()
//          .padWithWater(10)
//          .generateChunk();
//
const TILE_SIZE = 32;   // TODO: this should be configured with the tile set to be flexible

class ChunkBuilder {
    // Static Initializers
    static newEmpty() {
        return new ChunkBuilder();
    }

    // Modifier API
    fillSquare(tileType, {top = 0, left = 0, width = 1, height = 1} = {}) { // jshint ignore:line
        const totalWidth = left + width;
        const totalHeight = top + height;

        while (this.height < totalHeight) {
            this.addRow();
        }

        while (this.width < totalWidth) {
            this.addColumn();
        }

        for (let x = left; x < totalWidth; x++) {
            for (let y = top; y < totalHeight; y++) {
                this.grid[y][x].tile = tileType;
            }
        }

        return this;
    }

    addSimpleHouse(top = 0, left = 0) {
        const X = x => x + left;
        const Y = y => y + top;
        const grid = (y, x) => this.grid[Y(y)][X(x)];
        const tile = (y, x, tile) => grid(y, x).tile = tile;

        // first row
        tile(0, 1, 'P03');
        tile(0, 2, 'S03');

        // second row
        tile(1, 0, 'P03');
        tile(1, 1, 'P04');
        tile(1, 2, 'S04');
        tile(1, 3, 'S03');

        return this;
    }

    addCoast() {
        // TODO: This is very locked into the one tileset, we should generalize this sort of logic at some point

        // Grass to Shallow water transition
        this.addRow(true, 'H23');
        this.addRow(false, 'H25');
        this.addColumn(true,'G24');
        this.addColumn(false, 'I24');
        this.topLeft().tile = 'G23';
        this.bottomLeft().tile = 'G25';
        this.topRight().tile = 'I23';
        this.bottomRight().tile = 'I25';

        // Shallow water to deep water transition
        this.addRow(true, 'H19');
        this.addRow(false, 'H17');
        this.addColumn(true,'I18');
        this.addColumn(false, 'G18');
        this.topLeft().tile = 'C17';
        this.bottomLeft().tile = 'A17';
        this.topRight().tile = 'D17';
        this.bottomRight().tile = 'B17';

        return this;
    }

    padWithWater(paddingDepth = 1) {
        const water = 'F22';

        for (let i = 0; i < paddingDepth; i++) {
            this.addRow(true, water);
            this.addRow(false, water);
            this.addColumn(true, water);
            this.addColumn(false, water);
        }

        return this;
    }

    generateChunk() {
        const tilePositions = [];

        // Grab a top level clone of the grid array and reverse it to match the inverted display coordinates used by the client for rendering.
        const grid = this.grid.slice(0);
        grid.reverse();

        for (let x = 0; x < this.width; x++) {
            for (let y = 0; y < this.height; y++) {
                const cell = grid[y][x];

                if (cell.tile) {
                    tilePositions.push({
                        type: cell.tile,
                        position: {
                            x: x * TILE_SIZE,
                            y: y * TILE_SIZE
                        }
                    });
                }
            }
        }

        return tilePositions;
    }

    // Private helpers
    constructor() {
        this.width = 1;
        this.height = 1;
        this.grid = [[new Cell()]];   // initialize to a 1x1 matrix
    }

    addRow(atTop = false, tile = null) {
        const row = [];
        for (let i = 0; i < this.width; i++) {
            row.push(new Cell(tile));
        }

        if (atTop) {
            this.grid.unshift(row);
        } else {
            this.grid.push(row);
        }

        this.height++;
    }

    addColumn(atFront = false, tile = null) {
        this.grid.forEach(
            atFront ?
                row => row.unshift(new Cell(tile)) :
                row => row.push(new Cell(tile))
        );
        this.width++;
    }

    topLeft() {
        return this.grid[0][0];
    }

    topRight() {
        return this.grid[0][this.width - 1];
    }

    bottomLeft() {
        return this.grid[this.height - 1][0];
    }

    bottomRight() {
        return this.grid[this.height - 1][this.width - 1];
    }
}

class Cell {
    constructor(tile) {
        this.tile = tile;
        this.clutter = null;    // not implemented
        this.passable = true;   // not implemented
    }
}

export default ChunkBuilder;<|endoftext|>


<commit_before>/*jshint camelcase:false */
/**
 * @fileOverview The bare CRUD command implementations
 *
 */
var mime = require('mime');

var Controller = require('./controller-base');
var PaginationMidd = require('./pagination.midd');

var Create = require('./crud-ops/create');
var Read = require('./crud-ops/read');
var Update = require('./crud-ops/update');
var Delete = require('./crud-ops/delete');

/**
 * The CRUD Commands
 *
 * @contructor
 * @extends {crude.Controller}
 */
var CrudCmd = module.exports = Controller.extend(function(){
  /** @type {app.ctrl.Create} Instance of Create op */
  this.createOp = new Create(this);
  /** @type {app.ctrl.Read} Instance of Read op */
  this.readOp = new Read(this);
  /** @type {app.ctrl.Update} Instance of Update op */
  this.updateOp = new Update(this);
  /** @type {app.ctrl.Delete} Instance of Delete op */
  this.deleteOp = new Delete(this);

  /** @type {Object} Options, look at setOptions() */
  this.opts = {};

  /** @type {?Entity} An instance of Entity, use setEntity() */
  this.entity = null;

  // prep pagination
  this.pagination = new PaginationMidd();

  // define CRUD handlers
  this.create = [
    this._prepResponse.bind(this),
    this.createOp.create.bind(this.createOp),
  ];
  this.createView = [
    this._prepResponse.bind(this),
    this.createOp.createView.bind(this.createOp),
  ];

  this.readList = [];
  this.readOp.setupReadList();

  this.readOne = [
    this._prepResponse.bind(this),
    this.readOp.readOne.bind(this.readOp),
  ];
  this.update = [
    this._prepResponse.bind(this),
    this.updateOp.update.bind(this.updateOp),
  ];
  this.updateView = [
    this._prepResponse.bind(this),
    this.updateOp.updateView.bind(this.updateOp),
  ];
  this.delete = [this.deleteOp.delete.bind(this.deleteOp)];
});

/** @define {string} The view key in which the output will be available.  */
CrudCmd.VIEW_OUTPUT_KEY = 'crudView';

/**
 * Prepare the response object for each request, an internal middleware.
 *
 * @param {Object} req The request Object.
 * @param {Object} res The response Object.
 * @protected
 */
CrudCmd.prototype._prepResponse = function() {
  throw new Error('Not Implemented');
};

/**
 * Handle a successful outcome by either rendering or JSONing.
 *
 * @param {Object} req The request Object.
 * @param {Object} res The response Object.
 * @param {Object} doc The document item.
 * @private
 */
CrudCmd.prototype._handleSuccess = function(req, res, doc) {
  if (this.isJson(req)) {
    var sanitizedDoc = this._sanitizeResult(doc);
    res.json(200, sanitizedDoc);
  } else {
    // assign the item to the tpl vars.
    res.locals.item = doc;
    // construct the item's url
    // var itemUrl = path.join(this.getBaseUrl(req, true), doc[this.opts.urlField]);
    // res.locals.item.itemUrl = path.normalize(itemUrl) + '/';

    this.checkFlashSuccess(req, res);

    if (this.opts.itemView) {
      return res.render(this.opts.itemView);
    }

    // render the template and store in response locals.
    res.locals[CrudCmd.VIEW_OUTPUT_KEY] = this.compiled.view(res.locals);
    if (this.opts.layoutView) {
      res.render(this.opts.layoutView);
    } else {
      res.send(res.locals[CrudCmd.VIEW_OUTPUT_KEY]);
    }
  }
};

/**
 * Handle an success outcome properly depending on request Content-Type
 *
 * @param {Object} req The request Object.
 * @param {Object} res The response Object.
 * @param {Object} doc A document.
 * @private
 */
CrudCmd.prototype._handleSuccessRedirect = function(req, res, doc) {
  if (this.isJson(req)) {
    res.json(200, this._sanitizeResult(doc));
  } else {
    res.redirect(this.getBaseUrl(req, true) + '/' + doc[this.opts.urlField]);
  }
};

/**
 * Determine if request accepts JSON.
 *
 * @param {Object} req The request Object.
 * @return {boolean} yes or no.
 */
CrudCmd.prototype.isJson = function(req) {
  if (this.opts.noViews) {
    return true;
  }
  var acceptHeader = req.header('Accept');
  if (acceptHeader) {
    return mime.extension(acceptHeader) === 'json';
  }
  return false;
};

/**
 * Handle an error properly depending on request Content-Type
 *
 * @param {Object} req The request Object.
 * @param {Object} res The response Object.
 * @param {string} redirectUrl Define a redirect url.
 * @param {Error} err Error.
 * @throws Error whatever came through.
 */
CrudCmd.prototype._handleErrorRedirect = function(req, res, redirectUrl, err) {
  var acceptHeader = req.header('Accept');
  var accepts = null;
  if (acceptHeader) {
    accepts = mime.extension(acceptHeader);
  }
  if (this.opts.noViews) {
    accepts = 'json';
  }
  switch(accepts) {
  case 'json':
    res.status(401).json(this.jsonError(err));
    break;
  default:
    this.addFlashError(req, err);
    res.redirect(redirectUrl);
    break;
  }
};

/**
 * Clean the provided document based on 'canShow' schema property,
 * Check for opts.sanitizeResult callback and invoke it.
 *
 * @param {Object} doc The document.
 * @return {Object} A cleaned document.
 */
CrudCmd.prototype._sanitizeResult = function(doc) {
  if (typeof this.opts.sanitizeResult === 'function') {
    return this.opts.sanitizeResult(doc);
  } else {
    var schema = this.entity.getSchema();
    // check for showid and override canshow
    if (this.opts.showId) {
      schema[this.opts['idField']].canShow = true;
    }

    var sanitizedDoc = Object.create(null);
    Object.keys(schema).forEach(function(schemaKey) {
      if (schema[schemaKey].canShow) {
        sanitizedDoc[schemaKey] = doc[schemaKey];
      }
    });

    return sanitizedDoc;
  }
};

/**
 * Clean an array of items.
 *
 * @param {Array.<Object>} items Multiple Items.
 * @return {Array.<Object>} Cleaned items.
 */
CrudCmd.prototype.hidePrivatesArray = function(items) {
  return items.map(this._sanitizeResult, this);
};

/**
 * Produce normalized errors.
 *
 * @param {string|Error} err The error message.
 * @return {Object} a normalized error object.
 */
CrudCmd.prototype.jsonError = function(err) {
  var error = {
    error: true,
    message: '',
  };

  if (typeof err === 'string') {
    error.message = err;
  } else {
    // there's probably a better way than this to infer if the error object
    // will produce any keys when stringified...
    if (Object.keys(JSON.parse(JSON.stringify(err))).length === 0) {
      error.message = err.message;
    } else {
      error = err;
    }
  }

  return error;
};
<commit_msg>defence on schema<commit_after>/*jshint camelcase:false */
/**
 * @fileOverview The bare CRUD command implementations
 *
 */
var mime = require('mime');

var Controller = require('./controller-base');
var PaginationMidd = require('./pagination.midd');

var Create = require('./crud-ops/create');
var Read = require('./crud-ops/read');
var Update = require('./crud-ops/update');
var Delete = require('./crud-ops/delete');

/**
 * The CRUD Commands
 *
 * @contructor
 * @extends {crude.Controller}
 */
var CrudCmd = module.exports = Controller.extend(function(){
  /** @type {app.ctrl.Create} Instance of Create op */
  this.createOp = new Create(this);
  /** @type {app.ctrl.Read} Instance of Read op */
  this.readOp = new Read(this);
  /** @type {app.ctrl.Update} Instance of Update op */
  this.updateOp = new Update(this);
  /** @type {app.ctrl.Delete} Instance of Delete op */
  this.deleteOp = new Delete(this);

  /** @type {Object} Options, look at setOptions() */
  this.opts = {};

  /** @type {?Entity} An instance of Entity, use setEntity() */
  this.entity = null;

  // prep pagination
  this.pagination = new PaginationMidd();

  // define CRUD handlers
  this.create = [
    this._prepResponse.bind(this),
    this.createOp.create.bind(this.createOp),
  ];
  this.createView = [
    this._prepResponse.bind(this),
    this.createOp.createView.bind(this.createOp),
  ];

  this.readList = [];
  this.readOp.setupReadList();

  this.readOne = [
    this._prepResponse.bind(this),
    this.readOp.readOne.bind(this.readOp),
  ];
  this.update = [
    this._prepResponse.bind(this),
    this.updateOp.update.bind(this.updateOp),
  ];
  this.updateView = [
    this._prepResponse.bind(this),
    this.updateOp.updateView.bind(this.updateOp),
  ];
  this.delete = [this.deleteOp.delete.bind(this.deleteOp)];
});

/** @define {string} The view key in which the output will be available.  */
CrudCmd.VIEW_OUTPUT_KEY = 'crudView';

/**
 * Prepare the response object for each request, an internal middleware.
 *
 * @param {Object} req The request Object.
 * @param {Object} res The response Object.
 * @protected
 */
CrudCmd.prototype._prepResponse = function() {
  throw new Error('Not Implemented');
};

/**
 * Handle a successful outcome by either rendering or JSONing.
 *
 * @param {Object} req The request Object.
 * @param {Object} res The response Object.
 * @param {Object} doc The document item.
 * @private
 */
CrudCmd.prototype._handleSuccess = function(req, res, doc) {
  if (this.isJson(req)) {
    var sanitizedDoc = this._sanitizeResult(doc);
    res.json(200, sanitizedDoc);
  } else {
    // assign the item to the tpl vars.
    res.locals.item = doc;
    // construct the item's url
    // var itemUrl = path.join(this.getBaseUrl(req, true), doc[this.opts.urlField]);
    // res.locals.item.itemUrl = path.normalize(itemUrl) + '/';

    this.checkFlashSuccess(req, res);

    if (this.opts.itemView) {
      return res.render(this.opts.itemView);
    }

    // render the template and store in response locals.
    res.locals[CrudCmd.VIEW_OUTPUT_KEY] = this.compiled.view(res.locals);
    if (this.opts.layoutView) {
      res.render(this.opts.layoutView);
    } else {
      res.send(res.locals[CrudCmd.VIEW_OUTPUT_KEY]);
    }
  }
};

/**
 * Handle an success outcome properly depending on request Content-Type
 *
 * @param {Object} req The request Object.
 * @param {Object} res The response Object.
 * @param {Object} doc A document.
 * @private
 */
CrudCmd.prototype._handleSuccessRedirect = function(req, res, doc) {
  if (this.isJson(req)) {
    res.json(200, this._sanitizeResult(doc));
  } else {
    res.redirect(this.getBaseUrl(req, true) + '/' + doc[this.opts.urlField]);
  }
};

/**
 * Determine if request accepts JSON.
 *
 * @param {Object} req The request Object.
 * @return {boolean} yes or no.
 */
CrudCmd.prototype.isJson = function(req) {
  if (this.opts.noViews) {
    return true;
  }
  var acceptHeader = req.header('Accept');
  if (acceptHeader) {
    return mime.extension(acceptHeader) === 'json';
  }
  return false;
};

/**
 * Handle an error properly depending on request Content-Type
 *
 * @param {Object} req The request Object.
 * @param {Object} res The response Object.
 * @param {string} redirectUrl Define a redirect url.
 * @param {Error} err Error.
 * @throws Error whatever came through.
 */
CrudCmd.prototype._handleErrorRedirect = function(req, res, redirectUrl, err) {
  var acceptHeader = req.header('Accept');
  var accepts = null;
  if (acceptHeader) {
    accepts = mime.extension(acceptHeader);
  }
  if (this.opts.noViews) {
    accepts = 'json';
  }
  switch(accepts) {
  case 'json':
    res.status(401).json(this.jsonError(err));
    break;
  default:
    this.addFlashError(req, err);
    res.redirect(redirectUrl);
    break;
  }
};

/**
 * Clean the provided document based on 'canShow' schema property,
 * Check for opts.sanitizeResult callback and invoke it.
 *
 * @param {Object} doc The document.
 * @return {Object} A cleaned document.
 */
CrudCmd.prototype._sanitizeResult = function(doc) {
  if (typeof this.opts.sanitizeResult === 'function') {
    return this.opts.sanitizeResult(doc);
  } else {
    var schema = this.entity.getSchema();
    // check for showid and override canshow
    if (this.opts.showId && schema[this.opts['idField']]) {
      schema[this.opts['idField']].canShow = true;
    }

    var sanitizedDoc = Object.create(null);
    Object.keys(schema).forEach(function(schemaKey) {
      if (schema[schemaKey].canShow) {
        sanitizedDoc[schemaKey] = doc[schemaKey];
      }
    });

    return sanitizedDoc;
  }
};

/**
 * Clean an array of items.
 *
 * @param {Array.<Object>} items Multiple Items.
 * @return {Array.<Object>} Cleaned items.
 */
CrudCmd.prototype.hidePrivatesArray = function(items) {
  return items.map(this._sanitizeResult, this);
};

/**
 * Produce normalized errors.
 *
 * @param {string|Error} err The error message.
 * @return {Object} a normalized error object.
 */
CrudCmd.prototype.jsonError = function(err) {
  var error = {
    error: true,
    message: '',
  };

  if (typeof err === 'string') {
    error.message = err;
  } else {
    // there's probably a better way than this to infer if the error object
    // will produce any keys when stringified...
    if (Object.keys(JSON.parse(JSON.stringify(err))).length === 0) {
      error.message = err.message;
    } else {
      error = err;
    }
  }

  return error;
};
<|endoftext|>


<commit_before>var nroonga = require('../wrapped-nroonga');
var ValidationError = require('../errors').ValidationError;
var FieldTypeValidationError = require('../errors').FieldTypeValidationError;

var MINIMUM_NAME_LENGTH =
      exports.MINIMUM_NAME_LENGTH =
      IndexField.MINIMUM_NAME_LENGTH = 3;
var MAXIMUM_NAME_LENGTH =
      exports.MAXIMUM_NAME_LENGTH =
      IndexField.MAXIMUM_NAME_LENGTH = 64;

var VALID_NAME_PATTERN =
      exports.VALID_NAME_PATTERN =
      IndexField.VALID_NAME_PATTERN = '[a-z][a-z0-9_]*';
var VALID_NAME_MATCHER =
      exports.VALID_NAME_MATCHER =
      IndexField.VALID_NAME_MATCHER = new RegExp('^' + VALID_NAME_PATTERN + '$');
var INVALID_COLUMN_NAME_CHARACTERS_MATCHER =
      exports.INVALID_COLUMN_NAME_CHARACTERS_MATCHER =
      IndexField.INVALID_COLUMN_NAME_CHARACTERS_MATCHER = /[^_a-z0-9]/g;

var VALID_FIELD_TYPES =
      IndexField.VALID_FIELD_TYPES =
      exports.VALID_FIELD_TYPES = [
        'text',
        'literal',
        'uint'
      ];

var RESERVED_NAMES =
      IndexField.RESERVED_NAMES =
      exports.RESERVED_NAMES = [
        'body',
        'docid',
        'text_relevance'
     ];
var RESERVED_COLUMN_NAMES =
      IndexField.RESERVED_COLUMN_NAMES =
      exports.RESERVED_COLUMN_NAMES = [
        '_key'
      ];

function assertValidFieldName(field) {
  if (typeof field != 'string') {
    var error = new Error('field name name must be a string');
    error.isValidationError = true;
    throw error;
  }

  var errors = [];
  var commonPrefix = 'Value \'' + field + '\' at \'%NAME_FIELD%\' failed ' +
                     'to satisfy constraint: ';

  if (!field.match(VALID_NAME_MATCHER)) {
    errors.push(commonPrefix + 'Member must satisfy regular ' +
                'expression pattern: ' + VALID_NAME_PATTERN);
  } else {
    var invalidCharacters = field.match(INVALID_COLUMN_NAME_CHARACTERS_MATCHER);
    if (invalidCharacters) {
      invalidCharacters = Array.prototype.slice.call(invalidCharacters, 0)
                            .map(function(aCharacter) {
                              return "'" + aCharacter + "'";
                            });
      errors.push(commonPrefix + 'Member cannot include these ' +
                  'characters: ' + invalidCharacters.join(', '));
    }
  }

  if (field.length < MINIMUM_NAME_LENGTH)
    errors.push(commonPrefix + 'Member must have length greater ' +
                'than or equal to ' + MINIMUM_NAME_LENGTH);

  if (field.length > MAXIMUM_NAME_LENGTH)
    errors.push(commonPrefix + 'Member must have length less ' +
                'than or equal to ' + MAXIMUM_NAME_LENGTH);

  var index = exports.RESERVED_NAMES.indexOf(field);
  if (index > -1) index = exports.RESERVED_COLUMN_NAMES.indexOf(field);
  if (index > -1)
    errors.push(field + ' is a reserved field name');

  if (errors.length) {
    var prefix = errors.length > 1 ?
                   errors.length + ' validation errors detected: ' :
                   '1 validation error detected: ';
    throw new ValidationError(prefix + errors.join('; '));
  }
}

function assertValidFieldType(type) {
  var errors = [];
  var commonPrefix = 'Value \'' + type + '\' at \'%TYPE_FIELD%\' failed ' +
                     'to satisfy constraint: ';

  if (!type) {
    errors.push('Value null at \'%TYPE_FIELD%\' failed to satisfy ' +
                  'constraint: Member must not be null');
  } else {
    if (VALID_FIELD_TYPES.indexOf(type) < 0)
      errors.push(commonPrefix + 'Member must satisfy enum value set: [' +
                    VALID_FIELD_TYPES.join(', ') + ']');
  }

  if (errors.length) {
    var prefix = errors.length > 1 ?
                   errors.length + ' validation errors detected: ' :
                   '1 validation error detected: ';
    throw new ValidationError(prefix + errors.join('; '));
  }
}

function IndexField(name, domain) {
  this.domain = domain;
  this.context = domain.context;
  this.name = name;
  this._type = null;
  this.initialize();
};
IndexField.prototype = {
  initialize: function() {
    // for validation
    this.columnName;
  },
  get columnName() {
    if (!this._columnName) {
      assertValidFieldName(this.name);
      this._columnName = IndexField.toColumnNamePart(this.name);
    }
    return this._columnName;
  },
  get indexColumnName() {
    return this.domain.indexTableBaseName + this.columnName;
  },
  get indexTableName() {
    return this.domain.indexTableBaseName + this.columnName;
  },
  get indexTableKeyType() {
    var type = this.type;
    switch (type) {
      case 'uint':
        return nroonga.UInt32;
      case 'literal':
        return nroonga.ShortText;
      default:
        throw new Error('Unsupported index field type ' + type +
                        ' for index table');
    }
  },
  fieldTypeToColumnType: function(fieldType) {
    switch (fieldType) {
      case 'text':
        return nroonga.ShortText;
      case 'uint':
        return nroonga.UInt32;
      case 'literal':
        return this.indexTableName;
      default:
        throw new Error('Unsupported index field type ' + fieldType);
    }
  },
  columnTypeToFieldType: function(columnType) {
    switch (columnType) {
      case nroonga.ShortText:
        return 'text';
      case nroonga.UInt32:
        return 'uint';
      case this.indexTableName:
        return 'literal';
      default:
        throw new Error('Unsupported column type ' + columnType);
    }
  },

  get column() {
    var columns = this.context.ordinalColumnsSync(this.domain.tableName);
    for (var i = 0, maxi = columns.length; i < maxi; i++) {
      if (columns[i].name == this.columnName)
        return columns[i];
    }
    return null;
  },

  get type() {
    if (this._type)
      return this._type;

    var column = this.column;
    if (!column)
      throw new Error('the index field ' + this.name + ' is not created yet');

    if (column.type == 'var') {
      if (column.range == nroonga.ShortText)
        return this._type = 'text';
      if (column.range == this.indexTableName)
        return this._type = 'literal';
    } else if (column.type == 'fix') {
      if (column.range == nroonga.UInt32)
        return this._type = 'uint';
      else if (column.range == this.indexTableName)
        return this._type = 'literal';
    }

    throw new Error('unknown unfixed column ' + this.columnName);
  },
  set type(type) {
    assertValidFieldType(type);
    if (this.exists()) {
      if  (type != this.type)
        this.changeTypeToSync(type);
    } else {
      this._type = type;
    }
    return type;
  },
  setType: function(type) {
    this.type = type;
    return this;
  },

  get defaultValue() {
    return null;
  },

  get facetEnabled() {
    // uint fields cannot be drilldowned by groonga...
    if (this.type == 'uint')
      return false;

    if (!this.exists() || this._facetEnabled !== undefined)
      return !!this._facetEnabled;

    var value = this.domain.getConfiguration(this.facetEnabledConfigurationKey);
    return !!(this._facetEnabled = value);
  },
  set facetEnabled(value) {
    var booleanValue = !!value;
    if (booleanValue != this.facetEnabled && this.type == 'uint')
      throw new Error('facet option cannot be configured for the type ' + this.type + '.');

    this._facetEnabled = booleanValue;
    this.validateOptions();
    return value;
  },
  setFacetEnabled: function(value) {
    this.facetEnabled = value;
    return this;
  },
  get facetEnabledConfigurationKey() {
    return 'column_' + this.name + '_option_facet_enabled';
  },

  get resultEnabled() {
    if (this.type == 'uint')
      return true;

    if (!this.exists() || this._resultEnabled !== undefined)
      return !!this._resultEnabled;

    var value = this.domain.getConfiguration(this.resultEnabledConfigurationKey);
    return !!(this._resultEnabled = value);
  },
  set resultEnabled(value) {
    var booleanValue = !!value;
    if (booleanValue != this.resultEnabled && this.type == 'uint')
      throw new Error('returnable option cannot be configured for the type ' + this.type + '.');

    this._resultEnabled = !!value;
    this.validateOptions();
    return value;
  },
  setResultEnabled: function(value) {
    this.resultEnabled = value;
    return this;
  },
  get resultEnabledConfigurationKey() {
    return 'column_' + this.name + '_option_result_enabled';
  },

  get searchEnabled() {
    if (this.type == 'text' || this.type == 'uint')
      return true;

    if (!this.exists() || this._searchEnabled !== undefined)
      return !!this._searchEnabled;

    var value = this.domain.getConfiguration(this.searchEnabledConfigurationKey);
    return !!(this._searchEnabled = value);
  },
  set searchEnabled(value) {
    var booleanValue = !!value;
    if (booleanValue != this.searchEnabled &&
        (this.type == 'text' || this.type == 'uint'))
      throw new Error('searchable option cannot be configured for the type ' + this.type + '.');

    this._searchEnabled = booleanValue;
    this.validateOptions();
    return value;
  },
  setSearchEnabled: function(value) {
    this.searchEnabled = value;
    return this;
  },
  get searchEnabledConfigurationKey() {
    return 'column_' + this.name + '_option_search_enabled';
  },

  get defaultSearchField() {
    if (!this.exists())
      return false;

    return this == this.domain.defaultSearchField;
  },
  set defaultSearchField(value) {
    this._defaultSearchField = !!value;
    return value;
  },
  setDefaultSearchField: function(value) {
    this.defaultSearchField = value;
    return value;
  },

  get options() {
    var options = [];
    if (this.searchEnabled) options.push('Search');
    if (this.facetEnabled) options.push('Facet');
    if (this.resultEnabled) options.push('Result');
    return options.join(' ');
  },

  get state() {
    return 'Active';
  },

  get summary() {
    return this.name + ' ' + this.state + ' ' +
             this.type + ' (' + this.options + ')';
  },

  get multipleValues() {
    return !!this.column &&
           this.column.flags.indexOf(nroonga.COLUMN_VECTOR) > -1;
  },

  createSync: function(multipleValues) {
    var indexTableName = this.domain.termsTableName;

    var type = this.type;
    var columnType = this.fieldTypeToColumnType(type);

    if (type == 'uint' || type == 'literal') {
      this.context.commandSync('table_create', {
        name: this.indexTableName,
        flags: nroonga.TABLE_PAT_KEY,
        key_type: this.indexTableKeyType
      });
      indexTableName = this.indexTableName;
    }

    var columnFlags = multipleValues ?
                        nroonga.COLUMN_VECTOR : nroonga.COLUMN_SCALAR;
    this.context.commandSync('column_create', {
      table: this.domain.tableName,
      name: this.columnName,
      flags: columnFlags,
      type: columnType
    });
    this.context.commandSync('column_create', {
      table: indexTableName,
      name: this.indexColumnName,
      flags: nroonga.INDEX_COLUMN_DEFAULT_FLAGS,
      type: this.domain.tableName,
      source: this.columnName
    });

    this.saveOptionsSync();

    return this;
  },
  saveOptionsSync: function() {
    if (!this.exists()) return this;

    this.facetEnabled;
    if (this._facetEnabled !== undefined)
      this.domain.setConfiguration(this.facetEnabledConfigurationKey,
                                   this._facetEnabled);

    this.resultEnabled;
    if (this._resultEnabled !== undefined)
      this.domain.setConfiguration(this.resultEnabledConfigurationKey,
                                   this._resultEnabled);

    this.searchEnabled;
    if (this._searchEnabled !== undefined)
      this.domain.setConfiguration(this.searchEnabledConfigurationKey,
                                   this._searchEnabled);

    if (this._defaultSearchField !== undefined) {
      this.domain.defaultSearchField = this._defaultSearchField ? this : null ;
      delete this._defaultSearchField;
    }

    return this;
  },
  deleteSync: function() {
    // backup information for re-creation
    this._type = this.type;
    this._facetEnabled = this.facetEnabled;
    this._resultEnabled = this.resultEnabled;
    this._searchEnabled = this.searchEnabled;
    this._defaultSearchField = this.defaultSearchField;

    if (this._type == 'uint' || this._type == 'literal') {
      this.context.commandSync('table_remove', {
        name: this.indexTableName
      });
    }
    this.context.commandSync('column_remove', {
      table: this.domain.tableName,
      name: this.columnName
    });

    this.domain.deleteConfiguration(this.facetEnabledConfigurationKey);
    this.domain.deleteConfiguration(this.resultEnabledConfigurationKey);
    this.domain.deleteConfiguration(this.searchEnabledConfigurationKey);
    if (this._defaultSearchField)
      this.domain.defaultSearchField = null;

    return this;
  },
  reindexSync: function() {
    var name = this.name;
    var type = this.type;
    if (type == 'uint' || type == 'literal') {
      this.context.commandSync('column_remove', {
        table: this.indexTableName,
        name: this.indexColumnName
      });
      this.context.commandSync('table_remove', {
        name: this.indexTableName
      });
      this.context.commandSync('table_create', {
        name: this.indexTableName,
        flags: nroonga.TABLE_PAT_KEY,
        key_type: this.indexTableKeyType
      });
      this.context.commandSync('column_create', {
        table: this.indexTableName,
        name: this.indexColumnName,
        flags: nroonga.INDEX_COLUMN_DEFAULT_FLAGS,
        type: this.domain.tableName,
        source: this.columnName
      });
    } else {
      this.context.commandSync('column_remove', {
        table: this.domain.termsTableName,
        name: this.indexColumnName
      });
      this.context.commandSync('column_create', {
        table: this.domain.termsTableName,
        name: this.indexColumnName,
        flags: nroonga.INDEX_COLUMN_DEFAULT_FLAGS,
        type: this.domain.tableName,
        source: this.columnName
      });
    }
    return this;
  },

  exists: function() {
    return !!this.column;
  },

  upgradeToMultipleValuesSync: function() {
    if (this.multipleValues) return this;
    var values = this.domain.dumpSync();
    this.deleteSync();
    this.createSync(true);
    this.domain.loadSync(values);
    return this;
  },

  changeTypeToSync: function(type) {
    var values = this.domain.dumpSync();
    this.deleteSync();
    this._type = type;
    this.createSync(true);
    var name = this.columnName;
    values.forEach(function(record) {
      var value = record[name];
      switch (type) {
        case 'text':
          record[name] = Array.isArray(value) ?
            value.map(this._toText, this) : this._toText(value);
          break;

        case 'literal':
          record[name] = Array.isArray(value) ?
            value.map(this._toLiteral, this) : this._toLiteral(value);
          break;

        case 'uint':
          record[name] = Array.isArray(value) ?
            value.map(this._toUInt, this) : this._toUInt(value);
          break;
      }
    }, this);
    this.domain.loadSync(values);
    return this;
  },
  _toText: function(value) {
    return value.toString();
  },
  _toLiteral: function(value) {
    return value.toString();
  },
  _toUInt: function(value) {
     value = parseInt(value);
     if (isNaN(value))
       value = 0;
     return value;
  },

  validateOptions: function() {
    if (this.facetEnabled && this.resultEnabled)
      throw new FieldTypeValidationError(
              'Error defining field: ' + this.name + '. ' +
              'An IndexField may not be both FacetEnabled and ResultEnabled');
  }
};

exports.IndexField = IndexField;

IndexField.toColumnNamePart = function(fieldName) {
  return fieldName;
};
IndexField.toFieldName = function(columnNamePart) {
  return columnNamePart;
};
<commit_msg>Don't convert to multiple value type field<commit_after>var nroonga = require('../wrapped-nroonga');
var ValidationError = require('../errors').ValidationError;
var FieldTypeValidationError = require('../errors').FieldTypeValidationError;

var MINIMUM_NAME_LENGTH =
      exports.MINIMUM_NAME_LENGTH =
      IndexField.MINIMUM_NAME_LENGTH = 3;
var MAXIMUM_NAME_LENGTH =
      exports.MAXIMUM_NAME_LENGTH =
      IndexField.MAXIMUM_NAME_LENGTH = 64;

var VALID_NAME_PATTERN =
      exports.VALID_NAME_PATTERN =
      IndexField.VALID_NAME_PATTERN = '[a-z][a-z0-9_]*';
var VALID_NAME_MATCHER =
      exports.VALID_NAME_MATCHER =
      IndexField.VALID_NAME_MATCHER = new RegExp('^' + VALID_NAME_PATTERN + '$');
var INVALID_COLUMN_NAME_CHARACTERS_MATCHER =
      exports.INVALID_COLUMN_NAME_CHARACTERS_MATCHER =
      IndexField.INVALID_COLUMN_NAME_CHARACTERS_MATCHER = /[^_a-z0-9]/g;

var VALID_FIELD_TYPES =
      IndexField.VALID_FIELD_TYPES =
      exports.VALID_FIELD_TYPES = [
        'text',
        'literal',
        'uint'
      ];

var RESERVED_NAMES =
      IndexField.RESERVED_NAMES =
      exports.RESERVED_NAMES = [
        'body',
        'docid',
        'text_relevance'
     ];
var RESERVED_COLUMN_NAMES =
      IndexField.RESERVED_COLUMN_NAMES =
      exports.RESERVED_COLUMN_NAMES = [
        '_key'
      ];

function assertValidFieldName(field) {
  if (typeof field != 'string') {
    var error = new Error('field name name must be a string');
    error.isValidationError = true;
    throw error;
  }

  var errors = [];
  var commonPrefix = 'Value \'' + field + '\' at \'%NAME_FIELD%\' failed ' +
                     'to satisfy constraint: ';

  if (!field.match(VALID_NAME_MATCHER)) {
    errors.push(commonPrefix + 'Member must satisfy regular ' +
                'expression pattern: ' + VALID_NAME_PATTERN);
  } else {
    var invalidCharacters = field.match(INVALID_COLUMN_NAME_CHARACTERS_MATCHER);
    if (invalidCharacters) {
      invalidCharacters = Array.prototype.slice.call(invalidCharacters, 0)
                            .map(function(aCharacter) {
                              return "'" + aCharacter + "'";
                            });
      errors.push(commonPrefix + 'Member cannot include these ' +
                  'characters: ' + invalidCharacters.join(', '));
    }
  }

  if (field.length < MINIMUM_NAME_LENGTH)
    errors.push(commonPrefix + 'Member must have length greater ' +
                'than or equal to ' + MINIMUM_NAME_LENGTH);

  if (field.length > MAXIMUM_NAME_LENGTH)
    errors.push(commonPrefix + 'Member must have length less ' +
                'than or equal to ' + MAXIMUM_NAME_LENGTH);

  var index = exports.RESERVED_NAMES.indexOf(field);
  if (index > -1) index = exports.RESERVED_COLUMN_NAMES.indexOf(field);
  if (index > -1)
    errors.push(field + ' is a reserved field name');

  if (errors.length) {
    var prefix = errors.length > 1 ?
                   errors.length + ' validation errors detected: ' :
                   '1 validation error detected: ';
    throw new ValidationError(prefix + errors.join('; '));
  }
}

function assertValidFieldType(type) {
  var errors = [];
  var commonPrefix = 'Value \'' + type + '\' at \'%TYPE_FIELD%\' failed ' +
                     'to satisfy constraint: ';

  if (!type) {
    errors.push('Value null at \'%TYPE_FIELD%\' failed to satisfy ' +
                  'constraint: Member must not be null');
  } else {
    if (VALID_FIELD_TYPES.indexOf(type) < 0)
      errors.push(commonPrefix + 'Member must satisfy enum value set: [' +
                    VALID_FIELD_TYPES.join(', ') + ']');
  }

  if (errors.length) {
    var prefix = errors.length > 1 ?
                   errors.length + ' validation errors detected: ' :
                   '1 validation error detected: ';
    throw new ValidationError(prefix + errors.join('; '));
  }
}

function IndexField(name, domain) {
  this.domain = domain;
  this.context = domain.context;
  this.name = name;
  this._type = null;
  this.initialize();
};
IndexField.prototype = {
  initialize: function() {
    // for validation
    this.columnName;
  },
  get columnName() {
    if (!this._columnName) {
      assertValidFieldName(this.name);
      this._columnName = IndexField.toColumnNamePart(this.name);
    }
    return this._columnName;
  },
  get indexColumnName() {
    return this.domain.indexTableBaseName + this.columnName;
  },
  get indexTableName() {
    return this.domain.indexTableBaseName + this.columnName;
  },
  get indexTableKeyType() {
    var type = this.type;
    switch (type) {
      case 'uint':
        return nroonga.UInt32;
      case 'literal':
        return nroonga.ShortText;
      default:
        throw new Error('Unsupported index field type ' + type +
                        ' for index table');
    }
  },
  fieldTypeToColumnType: function(fieldType) {
    switch (fieldType) {
      case 'text':
        return nroonga.ShortText;
      case 'uint':
        return nroonga.UInt32;
      case 'literal':
        return this.indexTableName;
      default:
        throw new Error('Unsupported index field type ' + fieldType);
    }
  },
  columnTypeToFieldType: function(columnType) {
    switch (columnType) {
      case nroonga.ShortText:
        return 'text';
      case nroonga.UInt32:
        return 'uint';
      case this.indexTableName:
        return 'literal';
      default:
        throw new Error('Unsupported column type ' + columnType);
    }
  },

  get column() {
    var columns = this.context.ordinalColumnsSync(this.domain.tableName);
    for (var i = 0, maxi = columns.length; i < maxi; i++) {
      if (columns[i].name == this.columnName)
        return columns[i];
    }
    return null;
  },

  get type() {
    if (this._type)
      return this._type;

    var column = this.column;
    if (!column)
      throw new Error('the index field ' + this.name + ' is not created yet');

    if (column.type == 'var') {
      if (column.range == nroonga.ShortText)
        return this._type = 'text';
      if (column.range == this.indexTableName)
        return this._type = 'literal';
    } else if (column.type == 'fix') {
      if (column.range == nroonga.UInt32)
        return this._type = 'uint';
      else if (column.range == this.indexTableName)
        return this._type = 'literal';
    }

    throw new Error('unknown unfixed column ' + this.columnName);
  },
  set type(type) {
    assertValidFieldType(type);
    if (this.exists()) {
      if  (type != this.type)
        this.changeTypeToSync(type);
    } else {
      this._type = type;
    }
    return type;
  },
  setType: function(type) {
    this.type = type;
    return this;
  },

  get defaultValue() {
    return null;
  },

  get facetEnabled() {
    // uint fields cannot be drilldowned by groonga...
    if (this.type == 'uint')
      return false;

    if (!this.exists() || this._facetEnabled !== undefined)
      return !!this._facetEnabled;

    var value = this.domain.getConfiguration(this.facetEnabledConfigurationKey);
    return !!(this._facetEnabled = value);
  },
  set facetEnabled(value) {
    var booleanValue = !!value;
    if (booleanValue != this.facetEnabled && this.type == 'uint')
      throw new Error('facet option cannot be configured for the type ' + this.type + '.');

    this._facetEnabled = booleanValue;
    this.validateOptions();
    return value;
  },
  setFacetEnabled: function(value) {
    this.facetEnabled = value;
    return this;
  },
  get facetEnabledConfigurationKey() {
    return 'column_' + this.name + '_option_facet_enabled';
  },

  get resultEnabled() {
    if (this.type == 'uint')
      return true;

    if (!this.exists() || this._resultEnabled !== undefined)
      return !!this._resultEnabled;

    var value = this.domain.getConfiguration(this.resultEnabledConfigurationKey);
    return !!(this._resultEnabled = value);
  },
  set resultEnabled(value) {
    var booleanValue = !!value;
    if (booleanValue != this.resultEnabled && this.type == 'uint')
      throw new Error('returnable option cannot be configured for the type ' + this.type + '.');

    this._resultEnabled = !!value;
    this.validateOptions();
    return value;
  },
  setResultEnabled: function(value) {
    this.resultEnabled = value;
    return this;
  },
  get resultEnabledConfigurationKey() {
    return 'column_' + this.name + '_option_result_enabled';
  },

  get searchEnabled() {
    if (this.type == 'text' || this.type == 'uint')
      return true;

    if (!this.exists() || this._searchEnabled !== undefined)
      return !!this._searchEnabled;

    var value = this.domain.getConfiguration(this.searchEnabledConfigurationKey);
    return !!(this._searchEnabled = value);
  },
  set searchEnabled(value) {
    var booleanValue = !!value;
    if (booleanValue != this.searchEnabled &&
        (this.type == 'text' || this.type == 'uint'))
      throw new Error('searchable option cannot be configured for the type ' + this.type + '.');

    this._searchEnabled = booleanValue;
    this.validateOptions();
    return value;
  },
  setSearchEnabled: function(value) {
    this.searchEnabled = value;
    return this;
  },
  get searchEnabledConfigurationKey() {
    return 'column_' + this.name + '_option_search_enabled';
  },

  get defaultSearchField() {
    if (!this.exists())
      return false;

    return this == this.domain.defaultSearchField;
  },
  set defaultSearchField(value) {
    this._defaultSearchField = !!value;
    return value;
  },
  setDefaultSearchField: function(value) {
    this.defaultSearchField = value;
    return value;
  },

  get options() {
    var options = [];
    if (this.searchEnabled) options.push('Search');
    if (this.facetEnabled) options.push('Facet');
    if (this.resultEnabled) options.push('Result');
    return options.join(' ');
  },

  get state() {
    return 'Active';
  },

  get summary() {
    return this.name + ' ' + this.state + ' ' +
             this.type + ' (' + this.options + ')';
  },

  get multipleValues() {
    return !!this.column &&
           this.column.flags.indexOf(nroonga.COLUMN_VECTOR) > -1;
  },

  createSync: function(multipleValues) {
    var indexTableName = this.domain.termsTableName;

    var type = this.type;
    var columnType = this.fieldTypeToColumnType(type);

    if (type == 'uint' || type == 'literal') {
      this.context.commandSync('table_create', {
        name: this.indexTableName,
        flags: nroonga.TABLE_PAT_KEY,
        key_type: this.indexTableKeyType
      });
      indexTableName = this.indexTableName;
    }

    var columnFlags = multipleValues ?
                        nroonga.COLUMN_VECTOR : nroonga.COLUMN_SCALAR;
    this.context.commandSync('column_create', {
      table: this.domain.tableName,
      name: this.columnName,
      flags: columnFlags,
      type: columnType
    });
    this.context.commandSync('column_create', {
      table: indexTableName,
      name: this.indexColumnName,
      flags: nroonga.INDEX_COLUMN_DEFAULT_FLAGS,
      type: this.domain.tableName,
      source: this.columnName
    });

    this.saveOptionsSync();

    return this;
  },
  saveOptionsSync: function() {
    if (!this.exists()) return this;

    this.facetEnabled;
    if (this._facetEnabled !== undefined)
      this.domain.setConfiguration(this.facetEnabledConfigurationKey,
                                   this._facetEnabled);

    this.resultEnabled;
    if (this._resultEnabled !== undefined)
      this.domain.setConfiguration(this.resultEnabledConfigurationKey,
                                   this._resultEnabled);

    this.searchEnabled;
    if (this._searchEnabled !== undefined)
      this.domain.setConfiguration(this.searchEnabledConfigurationKey,
                                   this._searchEnabled);

    if (this._defaultSearchField !== undefined) {
      this.domain.defaultSearchField = this._defaultSearchField ? this : null ;
      delete this._defaultSearchField;
    }

    return this;
  },
  deleteSync: function() {
    // backup information for re-creation
    this._type = this.type;
    this._facetEnabled = this.facetEnabled;
    this._resultEnabled = this.resultEnabled;
    this._searchEnabled = this.searchEnabled;
    this._defaultSearchField = this.defaultSearchField;

    if (this._type == 'uint' || this._type == 'literal') {
      this.context.commandSync('table_remove', {
        name: this.indexTableName
      });
    }
    this.context.commandSync('column_remove', {
      table: this.domain.tableName,
      name: this.columnName
    });

    this.domain.deleteConfiguration(this.facetEnabledConfigurationKey);
    this.domain.deleteConfiguration(this.resultEnabledConfigurationKey);
    this.domain.deleteConfiguration(this.searchEnabledConfigurationKey);
    if (this._defaultSearchField)
      this.domain.defaultSearchField = null;

    return this;
  },
  reindexSync: function() {
    var name = this.name;
    var type = this.type;
    if (type == 'uint' || type == 'literal') {
      this.context.commandSync('column_remove', {
        table: this.indexTableName,
        name: this.indexColumnName
      });
      this.context.commandSync('table_remove', {
        name: this.indexTableName
      });
      this.context.commandSync('table_create', {
        name: this.indexTableName,
        flags: nroonga.TABLE_PAT_KEY,
        key_type: this.indexTableKeyType
      });
      this.context.commandSync('column_create', {
        table: this.indexTableName,
        name: this.indexColumnName,
        flags: nroonga.INDEX_COLUMN_DEFAULT_FLAGS,
        type: this.domain.tableName,
        source: this.columnName
      });
    } else {
      this.context.commandSync('column_remove', {
        table: this.domain.termsTableName,
        name: this.indexColumnName
      });
      this.context.commandSync('column_create', {
        table: this.domain.termsTableName,
        name: this.indexColumnName,
        flags: nroonga.INDEX_COLUMN_DEFAULT_FLAGS,
        type: this.domain.tableName,
        source: this.columnName
      });
    }
    return this;
  },

  exists: function() {
    return !!this.column;
  },

  upgradeToMultipleValuesSync: function() {
    if (this.multipleValues) return this;
    var values = this.domain.dumpSync();
    this.deleteSync();
    this.createSync(true);
    this.domain.loadSync(values);
    return this;
  },

  changeTypeToSync: function(type) {
    var values = this.domain.dumpSync();
    var multipleValues = this.multipleValues;
    this.deleteSync();
    this._type = type;
    this.createSync(multipleValues);
    var name = this.columnName;
    values.forEach(function(record) {
      var value = record[name];
      switch (type) {
        case 'text':
          record[name] = multipleValues ?
            value.map(this._toText, this) : this._toText(value);
          break;

        case 'literal':
          record[name] = multipleValues ?
            value.map(this._toLiteral, this) : this._toLiteral(value);
          break;

        case 'uint':
          record[name] = multipleValues ?
            value.map(this._toUInt, this) : this._toUInt(value);
          break;
      }
    }, this);
    this.domain.loadSync(values);
    return this;
  },
  _toText: function(value) {
    return value.toString();
  },
  _toLiteral: function(value) {
    return value.toString();
  },
  _toUInt: function(value) {
     value = parseInt(value);
     if (isNaN(value))
       value = 0;
     return value;
  },

  validateOptions: function() {
    if (this.facetEnabled && this.resultEnabled)
      throw new FieldTypeValidationError(
              'Error defining field: ' + this.name + '. ' +
              'An IndexField may not be both FacetEnabled and ResultEnabled');
  }
};

exports.IndexField = IndexField;

IndexField.toColumnNamePart = function(fieldName) {
  return fieldName;
};
IndexField.toFieldName = function(columnNamePart) {
  return columnNamePart;
};
<|endoftext|>


<commit_before>
'use strict';
const ApplicationError = require('../util/application_error');
const symbols = require('./symbols');

const sInit = Symbol('_init');

/**
 * Defines an abstract DatabaseProvider - a mechanism
 * by which connections can be obtained and transactions
 * can be entered and exited for a particular database
 * provider such as MySQL (see ravel-mysql-provider for an
 * example implementation).
 */
class DatabaseProvider {
  /**
   * @param name The unique name of this DatabaseProvider
   */
  constructor(name) {
    this.name = name;
  }

  /**
   * Obtain a connection and start a transaction
   *
   * @return {Promise} resolved by the open connection
   */
  getTransactionConnection() {  //eslint-disable-line no-unused-vars
    return Promise.reject(new ApplicationError.NotImplemented(
      'DatabaseProvider ' + this.name + ' must implement getTransactionConnection()'));
  }

  /**
   * End a transaction and close the connection.
   * Rollback the transaction iff finalErr !== null.
   *
   * @param connection {Object} A connection object which was used throughout the transaction
   * @param shouldCommit {Boolean} If true, commit, otherwise rollback
   * @return {Promise} resolved, or rejected if there was an error while closing the connection.
   */
  exitTransaction(connection, shouldCommit) {  //eslint-disable-line no-unused-vars
    return Promise.reject(new ApplicationError.NotImplemented(
      'DatabaseProvider ' + this.name + ' must implement exitTransaction(connection, shouldCommit)'));
  }
}

// Called internally by Ravel
DatabaseProvider.prototype[sInit] = function(ravelInstance) {
  this.log = ravelInstance.Log.getLogger(this.name);
};

module.exports = function(Ravel) {
  // Ravel should provide DatabaseProvider superclass statically
  Ravel.DatabaseProvider = DatabaseProvider;

  /**
   * Performs database provider initialization in
   * Ravel.init()
   */
  Ravel.prototype[symbols.databaseProviderInit] = function() {
    const providers = this.get('database providers');

    for (let p of providers) {
      p[sInit](this);
    }
  };
};
<commit_msg>Finished docs for database provider [skip ci]<commit_after>
'use strict';
const ApplicationError = require('../util/application_error');
const symbols = require('./symbols');

const sInit = Symbol('_init');

/**
 * Defines an abstract DatabaseProvider - a mechanism
 * by which connections can be obtained and transactions
 * can be entered and exited for a particular database
 * provider such as MySQL (see ravel-mysql-provider for an
 * example implementation).
 */
class DatabaseProvider {
  /**
   * @param name The unique name of this DatabaseProvider
   */
  constructor(name) {
    this.name = name;
  }

  /**
   * Obtain a connection and start a transaction
   *
   * @return {Promise} resolved by the open connection
   */
  getTransactionConnection() {  //eslint-disable-line no-unused-vars
    return Promise.reject(new ApplicationError.NotImplemented(
      'DatabaseProvider ' + this.name + ' must implement getTransactionConnection()'));
  }

  /**
   * End a transaction and close the connection.
   * Rollback the transaction iff finalErr !== null.
   *
   * @param connection {Object} A connection object which was used throughout the transaction
   * @param shouldCommit {Boolean} If true, commit, otherwise rollback
   * @return {Promise} resolved, or rejected if there was an error while closing the connection.
   */
  exitTransaction(connection, shouldCommit) {  //eslint-disable-line no-unused-vars
    return Promise.reject(new ApplicationError.NotImplemented(
      'DatabaseProvider ' + this.name + ' must implement exitTransaction(connection, shouldCommit)'));
  }
}

/**
 * Called internally by Ravel to initialize all registered DatabaseProviders
 */
DatabaseProvider.prototype[sInit] = function(ravelInstance) {
  this.log = ravelInstance.Log.getLogger(this.name);
};

/**
 * Populates Ravel class with the DatabaseProvider superclass, as well as an init method for all
 * registered DatabaseProviders
 */
module.exports = function(Ravel) {
  // Ravel should provide DatabaseProvider superclass statically
  Ravel.DatabaseProvider = DatabaseProvider;

  /**
   * Performs database provider initialization in
   * Ravel.init()
   */
  Ravel.prototype[symbols.databaseProviderInit] = function() {
    const providers = this.get('database providers');

    for (let p of providers) {
      p[sInit](this);
    }
  };
};
<|endoftext|>