<commit_before>'use strict';

const fse = require('fs-extra');
const path = require('path');
const ejs = require('ejs');
const util = require('util');
const utils = require('../cli/utils');
const pkg = require('../package.json');
const xcode = require('xcode');
const plist = require('plist');
const { Storage } = require('@google-cloud/storage');
const storage = new Storage();
const buildPath = 'build';
const cordova = utils.cordova(buildPath);

const BUILD_NUMBER = process.env.TRAVIS_BUILD_NUMBER || '1';

async function run() {
  const config = ejs.render(fse.readFileSync('config.xml.template', 'utf-8'), {
    widget: {
      id: 'com.coinspace.wallet',
      version: pkg.version,
      iosCFBundleVersion: BUILD_NUMBER
    }
  });
  fse.writeFileSync(path.resolve(buildPath, 'config.xml'), config);

  /* eslint-disable max-len */
  cordova('platform add ios@6.1.0');
  cordova('plugin add cordova-plugin-geolocation@4.0.2');
  cordova('plugin add phonegap-plugin-barcodescanner@8.1.0');
  cordova('plugin add cordova-plugin-dialogs@2.0.2');
  cordova('plugin add cordova-plugin-inappbrowser@4.0.0');
  cordova('plugin add cordova-plugin-statusbar@2.4.3');
  cordova('plugin add cordova-plugin-x-socialsharing@5.6.8 --variable PHOTO_LIBRARY_USAGE_DESCRIPTION="This app uses your photo library to upload photos." --variable PHOTO_LIBRARY_ADD_USAGE_DESCRIPTION="This app saves images to your photo library."');
  cordova('plugin add cordova-plugin-touch-id@3.4.0 --variable FACEID_USAGE_DESCRIPTION="Used for easy authentication."');
  cordova('plugin add cordova-plugin-customurlscheme@5.0.1 --variable URL_SCHEME=coinspace');
  cordova('plugin add https://github.com/CoinSpace/cordova-plugin-zendesk#ad8f26f2a5bc6b8974ef32fef5b82967f5b4c83b');
  cordova('plugin add cordova-plugin-cookiemaster@1.0.5');
  cordova('plugin add cordova-plugin-3dtouch-shortcutitems@1.0.2');
  cordova('plugin add cordova-plugin-safariviewcontroller@1.6.0');
  cordova('plugin add cordova-plugin-inapp-review@1.1.0');

  var orientations = {
    'UISupportedInterfaceOrientations': [
      'UIInterfaceOrientationPortrait',
    ],
    'UISupportedInterfaceOrientations~ipad': [
      'UIInterfaceOrientationPortrait',
      'UIInterfaceOrientationLandscapeLeft',
      'UIInterfaceOrientationLandscapeRight',
    ],
  };
  updatePlist(path.join(buildPath, 'platforms/ios/Coin/Coin-Info.plist'), orientations);

  await addWatchApp();

  if (process.env.CI) {
    utils.shell(`set -o pipefail && xcodebuild -workspace Coin.xcworkspace -scheme Coin \
      -configuration AppStoreDistribution archive \
      -archivePath Coin.xcarchive | xcpretty`,
      { cwd: path.join(buildPath, 'platforms/ios') }
    );
    utils.shell(`set -o pipefail && xcodebuild -exportArchive -archivePath Coin.xcarchive \
      -exportOptionsPlist ../../../iosExportOptions.plist \
      -exportPath ../../../deploy | xcpretty`,
      { cwd: path.join(buildPath, 'platforms/ios') }
    );
    const destination = `${pkg.version}-${process.env.TRAVIS_BRANCH || 'local'}/${pkg.name}-${pkg.version}.ipa`;
    await storage.bucket('coinspace-travis-ci').upload('deploy/Coin.ipa', { destination });
  } else {
    utils.shell('open platforms/ios/Coin.xcworkspace', { cwd: buildPath });
  }
}

function updatePlist(plistPath, update) {
  var plistContent = plist.parse(fse.readFileSync(plistPath, 'utf-8'));
  const plistData = Object.assign({}, plistContent, update);
  fse.writeFileSync(plistPath, plist.build(plistData, {
    indent: '\t',
    offset: -1,
  }), 'utf-8');
}

async function addWatchApp() {
  utils.shell(`git clone https://github.com/CoinSpace/cs-watchapp-ios.git`, { cwd: buildPath });
  utils.shell(`cd cs-watchapp-ios && git checkout ca942f2afc90062b5100d90332f8acafdff85788 -q`, { cwd: buildPath });
  utils.shell(`ln -s ${path.resolve(buildPath, 'cs-watchapp-ios/WatchApp')} ./platforms/ios/WatchApp`, { cwd: buildPath });
  utils.shell(`ln -s ${path.resolve(buildPath, 'cs-watchapp-ios/WatchAppExtension')} ./platforms/ios/WatchAppExtension`, { cwd: buildPath });

  var projectPath = path.resolve(buildPath, 'platforms/ios/Coin.xcodeproj/project.pbxproj');
  var project = xcode.project(projectPath);
  await util.promisify(project.parse.bind(project))();

  const WatchApp = addFolderToProject('WatchApp', path.join(buildPath, 'platforms/ios'), project);
  const WatchAppExtension = addFolderToProject('WatchAppExtension', path.join(buildPath, 'platforms/ios'), project);

  function addFolderToProject(folderPath, basePath, project, isChild) {
    var folder = path.basename(folderPath);
    var watchAppPaths = fse.readdirSync(path.resolve(basePath, folderPath));
    var files = [];
    var childs = [];
    watchAppPaths.forEach((f) => {
      if (f.startsWith('.')) return;
      var s = fse.statSync(path.resolve(basePath, folderPath, f));
      if (s.isFile() || path.extname(f) === '.xcassets') {
        files.push(path.join(folderPath, f));
      } else {
        childs.push(addFolderToProject(path.join(folderPath, f), basePath, project, true));
      }
    });
    const group = project.addPbxGroup(files, path.basename(folder), '""');
    childs.forEach((child) => {
      files = files.concat(child.files);
      project.addToPbxGroup(child.group.uuid, group.uuid);
    });

    if (!isChild) {
      project.addToPbxGroup(group.uuid, project.findPBXGroupKey({ name: 'CustomTemplate' }));
    }
    return { group, files };
  }

  // add new targets
  var WatchAppTarget = project.addTarget('WatchApp', 'watch2_app', 'WatchApp', 'com.coinspace.wallet.watchapp');
  var WatchAppExtensionTarget = project.addTarget('WatchAppExtension', 'watch2_extension', 'WatchAppExtension', 'com.coinspace.wallet.watchapp.extension');

  // edit XCBuildConfiguration
  const pbxXCBuildConfigurationSection = project.pbxXCBuildConfigurationSection();
  Object.keys(pbxXCBuildConfigurationSection).forEach((key) => {
    const setting = pbxXCBuildConfigurationSection[key].buildSettings;
    if (!setting) return false;
    if (!setting['PRODUCT_BUNDLE_IDENTIFIER']) return false;

    setting['CODE_SIGN_STYLE'] = 'Manual';
    setting['DEVELOPMENT_TEAM'] = '3M4KWD4BUU';

    const name = pbxXCBuildConfigurationSection[key].name;
    if (setting['PRODUCT_NAME'] === '"WatchApp"' || setting['PRODUCT_NAME'] === '"WatchAppExtension"') {
      setting['SDKROOT'] = 'watchos';
      setting['MARKETING_VERSION'] = pkg.version;
      setting['CURRENT_PROJECT_VERSION'] = BUILD_NUMBER;
      setting['SWIFT_VERSION'] = '4.0';
      setting['TARGETED_DEVICE_FAMILY'] = '4';
      setting['WATCHOS_DEPLOYMENT_TARGET'] = '4.3';
      setting['ENABLE_BITCODE'] = 'YES';
      if (name === 'Release') {
        setting['SWIFT_OPTIMIZATION_LEVEL'] = '"-O"';
        setting['SWIFT_COMPILATION_MODE'] = 'wholemodule';
        setting['CODE_SIGN_IDENTITY'] = '"iPhone Distribution"';
      } else {
        setting['SWIFT_OPTIMIZATION_LEVEL'] = '"-Onone"';
      }
    }

    if (setting['PRODUCT_BUNDLE_IDENTIFIER'] === 'com.coinspace.wallet') {
      if (name === 'Release') {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = '"com.coinspace.wallet (production)"';
      } else {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = 'com.coinspace.wallet';
      }
    } else if (setting['PRODUCT_NAME'] === '"WatchApp"') {
      setting['CODE_SIGN_ENTITLEMENTS'] = '"WatchApp/CoinSpace WatchApp.entitlements"';
      setting['ASSETCATALOG_COMPILER_APPICON_NAME'] = 'AppIcon';
      if (name === 'Release') {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = '"com.coinspace.wallet.watchapp (production)"';
      } else {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = 'com.coinspace.wallet.watchapp';
      }
    } else if (setting['PRODUCT_NAME'] === '"WatchAppExtension"') {
      setting['SWIFT_OBJC_BRIDGING_HEADER'] = '""';
      setting['CODE_SIGN_ENTITLEMENTS'] = '"WatchAppExtension/CoinSpace WatchApp Extension.entitlements"';
      setting['ASSETCATALOG_COMPILER_COMPLICATION_NAME'] = 'Complication';
      if (name === 'Release') {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = '"com.coinspace.wallet.watchapp.extension (prod)"';
      } else {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = 'com.coinspace.wallet.watchapp.extension';
      }
    }
  });

  project.addBuildPhase(
    WatchApp.files.filter((f) => /(\.xcassets|\.storyboard)$/.test(f)),
    'PBXResourcesBuildPhase',
    'Resources',
    WatchAppTarget.uuid
  );

  project.addBuildPhase(WatchAppExtension.files.filter((f) => /\.swift$/.test(f)), 'PBXSourcesBuildPhase', 'Sources', WatchAppExtensionTarget.uuid);

  project.addBuildPhase(
    WatchAppExtension.files.filter((f) => /(\.xcassets)$/.test(f)),
    'PBXResourcesBuildPhase',
    'Resources',
    WatchAppExtensionTarget.uuid
  );

  fse.writeFileSync(projectPath, project.writeSync());

  // add pods
  var watchAppPodfile = fse.readFileSync(path.resolve(buildPath, 'cs-watchapp-ios/WatchAppExtension/Podfile'));
  fse.appendFileSync(path.resolve(buildPath, 'platforms/ios/Podfile'), watchAppPodfile);
  utils.shell(`pod install`, { cwd: path.join(buildPath, 'platforms/ios') });
}

process.on('unhandledRejection', function(err) {
  throw err;
});

run();
<commit_msg>feat(ios): update zendesk plugin<commit_after>'use strict';

const fse = require('fs-extra');
const path = require('path');
const ejs = require('ejs');
const util = require('util');
const utils = require('../cli/utils');
const pkg = require('../package.json');
const xcode = require('xcode');
const plist = require('plist');
const { Storage } = require('@google-cloud/storage');
const storage = new Storage();
const buildPath = 'build';
const cordova = utils.cordova(buildPath);

const BUILD_NUMBER = process.env.TRAVIS_BUILD_NUMBER || '1';

async function run() {
  const config = ejs.render(fse.readFileSync('config.xml.template', 'utf-8'), {
    widget: {
      id: 'com.coinspace.wallet',
      version: pkg.version,
      iosCFBundleVersion: BUILD_NUMBER
    }
  });
  fse.writeFileSync(path.resolve(buildPath, 'config.xml'), config);

  /* eslint-disable max-len */
  cordova('platform add ios@6.1.0');
  cordova('plugin add cordova-plugin-geolocation@4.0.2');
  cordova('plugin add phonegap-plugin-barcodescanner@8.1.0');
  cordova('plugin add cordova-plugin-dialogs@2.0.2');
  cordova('plugin add cordova-plugin-inappbrowser@4.0.0');
  cordova('plugin add cordova-plugin-statusbar@2.4.3');
  cordova('plugin add cordova-plugin-x-socialsharing@5.6.8 --variable PHOTO_LIBRARY_USAGE_DESCRIPTION="This app uses your photo library to upload photos." --variable PHOTO_LIBRARY_ADD_USAGE_DESCRIPTION="This app saves images to your photo library."');
  cordova('plugin add cordova-plugin-touch-id@3.4.0 --variable FACEID_USAGE_DESCRIPTION="Used for easy authentication."');
  cordova('plugin add cordova-plugin-customurlscheme@5.0.1 --variable URL_SCHEME=coinspace');
  cordova('plugin add https://github.com/CoinSpace/cordova-plugin-zendesk#269bdcca489a9a06d127ccc0999784d9c94c7171');
  cordova('plugin add cordova-plugin-cookiemaster@1.0.5');
  cordova('plugin add cordova-plugin-3dtouch-shortcutitems@1.0.2');
  cordova('plugin add cordova-plugin-safariviewcontroller@1.6.0');
  cordova('plugin add cordova-plugin-inapp-review@1.1.0');

  var orientations = {
    'UISupportedInterfaceOrientations': [
      'UIInterfaceOrientationPortrait',
    ],
    'UISupportedInterfaceOrientations~ipad': [
      'UIInterfaceOrientationPortrait',
      'UIInterfaceOrientationLandscapeLeft',
      'UIInterfaceOrientationLandscapeRight',
    ],
  };
  updatePlist(path.join(buildPath, 'platforms/ios/Coin/Coin-Info.plist'), orientations);

  await addWatchApp();

  if (process.env.CI) {
    utils.shell(`set -o pipefail && xcodebuild -workspace Coin.xcworkspace -scheme Coin \
      -configuration AppStoreDistribution archive \
      -archivePath Coin.xcarchive | xcpretty`,
      { cwd: path.join(buildPath, 'platforms/ios') }
    );
    utils.shell(`set -o pipefail && xcodebuild -exportArchive -archivePath Coin.xcarchive \
      -exportOptionsPlist ../../../iosExportOptions.plist \
      -exportPath ../../../deploy | xcpretty`,
      { cwd: path.join(buildPath, 'platforms/ios') }
    );
    const destination = `${pkg.version}-${process.env.TRAVIS_BRANCH || 'local'}/${pkg.name}-${pkg.version}.ipa`;
    await storage.bucket('coinspace-travis-ci').upload('deploy/Coin.ipa', { destination });
  } else {
    utils.shell('open platforms/ios/Coin.xcworkspace', { cwd: buildPath });
  }
}

function updatePlist(plistPath, update) {
  var plistContent = plist.parse(fse.readFileSync(plistPath, 'utf-8'));
  const plistData = Object.assign({}, plistContent, update);
  fse.writeFileSync(plistPath, plist.build(plistData, {
    indent: '\t',
    offset: -1,
  }), 'utf-8');
}

async function addWatchApp() {
  utils.shell(`git clone https://github.com/CoinSpace/cs-watchapp-ios.git`, { cwd: buildPath });
  utils.shell(`cd cs-watchapp-ios && git checkout ca942f2afc90062b5100d90332f8acafdff85788 -q`, { cwd: buildPath });
  utils.shell(`ln -s ${path.resolve(buildPath, 'cs-watchapp-ios/WatchApp')} ./platforms/ios/WatchApp`, { cwd: buildPath });
  utils.shell(`ln -s ${path.resolve(buildPath, 'cs-watchapp-ios/WatchAppExtension')} ./platforms/ios/WatchAppExtension`, { cwd: buildPath });

  var projectPath = path.resolve(buildPath, 'platforms/ios/Coin.xcodeproj/project.pbxproj');
  var project = xcode.project(projectPath);
  await util.promisify(project.parse.bind(project))();

  const WatchApp = addFolderToProject('WatchApp', path.join(buildPath, 'platforms/ios'), project);
  const WatchAppExtension = addFolderToProject('WatchAppExtension', path.join(buildPath, 'platforms/ios'), project);

  function addFolderToProject(folderPath, basePath, project, isChild) {
    var folder = path.basename(folderPath);
    var watchAppPaths = fse.readdirSync(path.resolve(basePath, folderPath));
    var files = [];
    var childs = [];
    watchAppPaths.forEach((f) => {
      if (f.startsWith('.')) return;
      var s = fse.statSync(path.resolve(basePath, folderPath, f));
      if (s.isFile() || path.extname(f) === '.xcassets') {
        files.push(path.join(folderPath, f));
      } else {
        childs.push(addFolderToProject(path.join(folderPath, f), basePath, project, true));
      }
    });
    const group = project.addPbxGroup(files, path.basename(folder), '""');
    childs.forEach((child) => {
      files = files.concat(child.files);
      project.addToPbxGroup(child.group.uuid, group.uuid);
    });

    if (!isChild) {
      project.addToPbxGroup(group.uuid, project.findPBXGroupKey({ name: 'CustomTemplate' }));
    }
    return { group, files };
  }

  // add new targets
  var WatchAppTarget = project.addTarget('WatchApp', 'watch2_app', 'WatchApp', 'com.coinspace.wallet.watchapp');
  var WatchAppExtensionTarget = project.addTarget('WatchAppExtension', 'watch2_extension', 'WatchAppExtension', 'com.coinspace.wallet.watchapp.extension');

  // edit XCBuildConfiguration
  const pbxXCBuildConfigurationSection = project.pbxXCBuildConfigurationSection();
  Object.keys(pbxXCBuildConfigurationSection).forEach((key) => {
    const setting = pbxXCBuildConfigurationSection[key].buildSettings;
    if (!setting) return false;
    if (!setting['PRODUCT_BUNDLE_IDENTIFIER']) return false;

    setting['CODE_SIGN_STYLE'] = 'Manual';
    setting['DEVELOPMENT_TEAM'] = '3M4KWD4BUU';

    const name = pbxXCBuildConfigurationSection[key].name;
    if (setting['PRODUCT_NAME'] === '"WatchApp"' || setting['PRODUCT_NAME'] === '"WatchAppExtension"') {
      setting['SDKROOT'] = 'watchos';
      setting['MARKETING_VERSION'] = pkg.version;
      setting['CURRENT_PROJECT_VERSION'] = BUILD_NUMBER;
      setting['SWIFT_VERSION'] = '4.0';
      setting['TARGETED_DEVICE_FAMILY'] = '4';
      setting['WATCHOS_DEPLOYMENT_TARGET'] = '4.3';
      setting['ENABLE_BITCODE'] = 'YES';
      if (name === 'Release') {
        setting['SWIFT_OPTIMIZATION_LEVEL'] = '"-O"';
        setting['SWIFT_COMPILATION_MODE'] = 'wholemodule';
        setting['CODE_SIGN_IDENTITY'] = '"iPhone Distribution"';
      } else {
        setting['SWIFT_OPTIMIZATION_LEVEL'] = '"-Onone"';
      }
    }

    if (setting['PRODUCT_BUNDLE_IDENTIFIER'] === 'com.coinspace.wallet') {
      if (name === 'Release') {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = '"com.coinspace.wallet (production)"';
      } else {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = 'com.coinspace.wallet';
      }
    } else if (setting['PRODUCT_NAME'] === '"WatchApp"') {
      setting['CODE_SIGN_ENTITLEMENTS'] = '"WatchApp/CoinSpace WatchApp.entitlements"';
      setting['ASSETCATALOG_COMPILER_APPICON_NAME'] = 'AppIcon';
      if (name === 'Release') {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = '"com.coinspace.wallet.watchapp (production)"';
      } else {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = 'com.coinspace.wallet.watchapp';
      }
    } else if (setting['PRODUCT_NAME'] === '"WatchAppExtension"') {
      setting['SWIFT_OBJC_BRIDGING_HEADER'] = '""';
      setting['CODE_SIGN_ENTITLEMENTS'] = '"WatchAppExtension/CoinSpace WatchApp Extension.entitlements"';
      setting['ASSETCATALOG_COMPILER_COMPLICATION_NAME'] = 'Complication';
      if (name === 'Release') {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = '"com.coinspace.wallet.watchapp.extension (prod)"';
      } else {
        setting['PROVISIONING_PROFILE_SPECIFIER'] = 'com.coinspace.wallet.watchapp.extension';
      }
    }
  });

  project.addBuildPhase(
    WatchApp.files.filter((f) => /(\.xcassets|\.storyboard)$/.test(f)),
    'PBXResourcesBuildPhase',
    'Resources',
    WatchAppTarget.uuid
  );

  project.addBuildPhase(WatchAppExtension.files.filter((f) => /\.swift$/.test(f)), 'PBXSourcesBuildPhase', 'Sources', WatchAppExtensionTarget.uuid);

  project.addBuildPhase(
    WatchAppExtension.files.filter((f) => /(\.xcassets)$/.test(f)),
    'PBXResourcesBuildPhase',
    'Resources',
    WatchAppExtensionTarget.uuid
  );

  fse.writeFileSync(projectPath, project.writeSync());

  // add pods
  var watchAppPodfile = fse.readFileSync(path.resolve(buildPath, 'cs-watchapp-ios/WatchAppExtension/Podfile'));
  fse.appendFileSync(path.resolve(buildPath, 'platforms/ios/Podfile'), watchAppPodfile);
  utils.shell(`pod install`, { cwd: path.join(buildPath, 'platforms/ios') });
}

process.on('unhandledRejection', function(err) {
  throw err;
});

run();
<|endoftext|>


<commit_before>const mongoose = require('mongoose')
  , async = require('async')
  , Event = require('./event')
  , log = require('winston');

const Schema = mongoose.Schema;

// Collection to hold unique observation ids
const ObservationIdSchema = new Schema();
const ObservationId = mongoose.model('ObservationId', ObservationIdSchema);

const FormSchema = new Schema({
  formId: { type: Number, required: true }
},{
  strict: false
});

const PropertiesSchema = new Schema({
  timestamp: { type: Date, required: true },
  forms: [FormSchema]
});

const StateSchema = new Schema({
  name: { type: String, required: true },
  userId: { type: Schema.Types.ObjectId, ref: 'User' }
});

const ThumbnailSchema = new Schema({
  contentType: { type: String, required: false },
  size: { type: Number, required: false },
  name: { type: String, required: false },
  relativePath: { type: String, required: true },
  minDimension: { type: Number, required: true },
  width: { type: Number, required: false },
  height: { type: Number, required: false}
},{
  strict: false
});

const AttachmentSchema = new Schema({
  observationFormId: { type: Schema.Types.ObjectId, required: true },
  fieldName: { type: String, required: true },
  lastModified: {type: Date, required: false},
  contentType: { type: String, required: false },
  size: { type: Number, required: false },
  name: { type: String, required: false },
  relativePath: { type: String, required: false },
  width: { type: Number, required: false },
  height: { type: Number, required: false },
  oriented: {type: Boolean, required: true, default: false },
  thumbnails: [ThumbnailSchema]
},{
  strict: false
});

// Creates the Schema for the Attachments object
const ObservationSchema = new Schema({
  type: { type: String, enum: ['Feature'], required: true},
  lastModified: {type: Date, required: false},
  userId: {type: Schema.Types.ObjectId, ref: 'User', required: false, sparse: true},
  deviceId: {type: Schema.Types.ObjectId, required: false, sparse: true},
  geometry: Schema.Types.Mixed,
  properties: PropertiesSchema,
  attachments: [AttachmentSchema],
  states: [StateSchema],
  important: {
    userId: {type: Schema.Types.ObjectId, ref: 'User', required: false},
    timestamp: {type: Date, required: false},
    description: {type: String, required: false}
  },
  favoriteUserIds: [{type: Schema.Types.ObjectId, ref: 'User'}]
},{
  strict: false,
  timestamps: {
    createdAt: 'createdAt',
    updatedAt: 'lastModified'
  }
});

ObservationSchema.index({geometry: "2dsphere"});
ObservationSchema.index({'lastModified': 1});
ObservationSchema.index({'attachments.lastModified': 1});
ObservationSchema.index({'userId': 1});
ObservationSchema.index({'deviceId': 1});
ObservationSchema.index({'properties.timestamp': 1});
ObservationSchema.index({'states.name': 1});
ObservationSchema.index({'attachments.oriented': 1});
ObservationSchema.index({'attachments.contentType': 1});
ObservationSchema.index({'attachments.thumbnails.minDimension': 1});

function transformAttachment(attachment, observation) {
  attachment.id = attachment._id;
  delete attachment._id;
  delete attachment.thumbnails;

  if (attachment.relativePath) {
    attachment.url = [observation.url, "attachments", attachment.id].join("/");
  }

  return attachment;
}

function transformState(state, observation) {
  state.id = state._id;
  delete state._id;

  state.url = [observation.url, "states", state.id].join("/");
  return state;
}

function transform(observation, ret, options) {
  if ('function' !== typeof observation.ownerDocument) {
    ret.id = ret._id;
    delete ret._id;
    delete ret.__v;

    if (options.event) {
      ret.eventId = options.event._id;
    }

    const path = options.path ? options.path : "";
    ret.url = [path, observation.id].join("/");

    if (observation.states && observation.states.length) {
      ret.state = transformState(ret.states[0], ret);
      delete ret.states;
    }

    if (observation.properties && observation.properties.forms) {
      ret.properties.forms = ret.properties.forms.map(observationForm => {
        observationForm.id = observationForm._id;
        delete observationForm._id;
        return observationForm;
      });
    }

    if (observation.attachments) {
      ret.attachments = ret.attachments.map(function (attachment) {
        return transformAttachment(attachment, ret);
      });
    }

    const populatedUserId = observation.populated('userId');
    if (populatedUserId) {
      ret.user = ret.userId;
      // TODO Update mobile clients to handle observation.userId or observation.user.id
      // Leave userId as mobile clients depend on it for observation create/update,
      ret.userId =  populatedUserId;
    }

    const populatedImportantUserId = observation.populated('important.userId')
    if (populatedImportantUserId && ret.important) {
      ret.important.user = ret.important.userId
      delete ret.important.userId;
    }
  }
}

ObservationIdSchema.set("toJSON", {
  transform: transform
});

ObservationSchema.set('toJSON', {
  transform: transform
});

ObservationSchema.set('toObject', {
  transform: transform
});

const models = {};
mongoose.model('Attachment', AttachmentSchema);
mongoose.model('Thumbnail', ThumbnailSchema);
mongoose.model('State', StateSchema);

// return a string for each property
function convertFieldForQuery(field, keys, fields) {
  keys = keys || [];
  fields = fields || {};

  for (const childField in field) {
    keys.push(childField);
    if (Object(field[childField]) === field[childField]) {
      convertFieldForQuery(field[childField], keys, fields);
    } else {
      const key = keys.join(".");
      if (field[childField]) {
        fields[key] = field[childField];
      }
      keys.pop();
    }
  }

  return fields;
}

function parseFields(fields) {
  if (fields) {
    const state = fields.state ? true : false;
    delete fields.state;

    fields = convertFieldForQuery(fields);
    if (fields.id === undefined) fields.id = true; // default is to return id if not specified
    if (fields.type === undefined) fields.type = true; // default is to return type if not specified

    if (state) {
      fields.states = {$slice: 1};
    }

    return fields;
  } else {
    return { states: {$slice: 1}};
  }
}

function observationModel(event) {
  const name = event.collectionName;
  let model = models[name];
  if (!model) {
    // Creates the Model for the Observation Schema
    model = mongoose.model(name, ObservationSchema, name);
    models[name] = model;
  }

  return model;
}

exports.observationModel = observationModel;

exports.getObservations = function(event, o, callback) {
  const conditions = {};

  const filter = o.filter || {};
  // Filter by geometry
  if (filter.geometry) {
    conditions.geometry = {
      $geoIntersects: {
        $geometry: filter.geometry
      }
    };
  }

  if (filter.startDate || filter.endDate) {
    conditions.lastModified = {};
    if (filter.startDate) {
      conditions.lastModified.$gte = filter.startDate;
    }

    if (filter.endDate) {
      conditions.lastModified.$lt = filter.endDate;
    }
  }

  if (filter.observationStartDate || filter.observationEndDate) {
    conditions['properties.timestamp'] = {};
    if (filter.observationStartDate) {
      conditions['properties.timestamp'].$gte = filter.observationStartDate;
    }

    if (filter.observationEndDate) {
      conditions['properties.timestamp'].$lt = filter.observationEndDate;
    }
  }

  if (filter.states) {
    conditions['states.0.name'] = { $in: filter.states };
  }

  if (filter.favorites && filter.favorites.userId) {
    conditions['favoriteUserIds'] = { $in: [filter.favorites.userId] };
  }

  if (filter.important) {
    conditions['important'] = {$exists: true};
  }

  const options = {};
  if (o.sort) {
    options.sort = o.sort;
  }

  const fields = parseFields(o.fields);
  if (filter.attachments === true) {
    fields.attachments = {$slice: 0};
  }

  let query = observationModel(event).find(conditions, fields, options);

  if (o.populate) {
    query = query
      .populate({
        path: 'userId',
        select: 'displayName' })
      .populate({
        path: 'important.userId',
        select: 'displayName'
      });
  }

  query.exec(callback);
};

exports.createObservationId = function(callback) {
  ObservationId.create({}, callback);
};

exports.getObservationId = function(id, callback) {
  ObservationId.findById(id, function(err, doc) {
    callback(err, doc ? doc._id : null);
  });
};

exports.getLatestObservation = function(event, callback) {
  observationModel(event).findOne({}, {lastModified: true}, {sort: {"lastModified": -1}, limit: 1}, callback);
};

exports.getObservationById = function(event, observationId, options, callback) {
  const fields = parseFields(options.fields);

  observationModel(event).findById(observationId, fields, callback);
};

exports.updateObservation = function(event, observationId, update, callback) {
  let addAttachments = [];
  let removeAttachments = [];
  update.properties.forms
    .map(observationForm => {
      observationForm._id = observationForm._id || mongoose.Types.ObjectId();
      delete observationForm.id;
      return observationForm;
    })
    .forEach(observationForm => {
      const formDefinition = event.forms.find(form => form._id === observationForm.formId);
      Object.keys(observationForm).forEach(fieldName => {
        const fieldDefinition = formDefinition.fields.find(field => field.name === fieldName);
        if (fieldDefinition && fieldDefinition.type === 'attachment') {
          const attachments = observationForm[fieldName] || [];
          addAttachments = addAttachments.concat(attachments
            .filter(attachment => attachment.action === 'add')
            .map(attachment => {
              return {
                observationFormId: observationForm._id,
                fieldName: fieldName,
                name: attachment.name,
                contentType: attachment.contentType
              }
            }));

          removeAttachments = removeAttachments.concat(attachments
            .filter(attachment => attachment.action === 'delete')
            .map(attachment => attachment.id )
          );

          delete observationForm[fieldName]
        }
      });
  });

  const ObservationModel = observationModel(event);
  ObservationModel.findById(observationId)
    .then(observation => {
      if (!observation) {
        observation = new ObservationModel({
          _id: observationId,
          userId: update.userId,
          deviceId: update.deviceId,
          states: [{ name: 'active', userId: update.userId }]
        });
      }

      observation.type = update.type;
      observation.geometry = update.geometry;
      observation.properties = update.properties;
      observation.attachments = observation.attachments.concat(addAttachments);
      observation.attachments = observation.attachments.filter(attachment => {
        return !removeAttachments.includes(attachment._id.toString())
      });

      return observation.save();
    })
    .then(observation => {
      return observation
        .populate({ path: 'userId', select: 'displayName' })
        .populate({ path: 'important.userId', select: 'displayName' })
        .execPopulate();
    })
    .then(observation => {
      callback(null, observation);
    })
    .catch(err => callback(err));
};

exports.removeObservation = function(event, observationId, callback) {
  observationModel(event).findByIdAndRemove(observationId, callback);
};

exports.removeUser = function(user, callback) {
  const condition = { userId: user._id };
  const update = { '$unset': { userId: true } };
  const options = { multi: true };

  Event.getEvents({}, function(err, events) {
    async.each(events, function(event, done) {
      observationModel(event).update(condition, update, options, function(err, numberAffected) {
        log.info('Remove deleted user from ' + numberAffected + ' documents for event ' + event.name);
        done();
      });
    },
    function(err){
      callback(err);
    });
  });
};

exports.removeDevice = function(device, callback) {
  const condition = { deviceId: device._id };
  const update = { '$unset': { deviceId: true } };
  const options = { multi: true };

  Event.getEvents(function(err, events) {
    async.each(events, function(event, done) {
      observationModel(event).update(condition, update, options, function(err, numberAffected) {
        log.info('Remove deleted device from ' + numberAffected + ' documents for event ' + event.name);
        done();
      });
    },
    function(err){
      callback(err);
    });
  });
};

exports.addState = function(event, id, state, callback) {
  const condition = { _id: mongoose.Types.ObjectId(id), 'states.0.name': { '$ne': state.name } };
  state._id = mongoose.Types.ObjectId();
  const update = {
    '$push': {
      states: {
        '$each': [state],
        '$position': 0
      }
    }
  };

  observationModel(event).update(condition, update, {upsert: false}, function(err) {
    callback(err, state);
  });
};

exports.addFavorite = function(event, observationId, user, callback) {
  const update = {
    $addToSet: {
      favoriteUserIds: user._id
    }
  };

  observationModel(event).findByIdAndUpdate(observationId, update, {new: true}, callback);
};

exports.removeFavorite = function (event, observationId, user, callback) {
  const update = {
    $pull: {
      favoriteUserIds: user._id
    }
  };

  observationModel(event).findByIdAndUpdate(observationId, update, {new: true}, callback);
};

exports.addImportant = function(event, observationId, important, callback) {
  const update = { important};
  observationModel(event).findByIdAndUpdate(observationId, update, { new: true }, callback);
}

exports.removeImportant = function(event, id, callback) {
  const update = {
    '$unset': {
      important: 1
    }
  };

  observationModel(event).findByIdAndUpdate(id, update, {new: true}, callback);
};

exports.getAttachment = function(event, observationId, attachmentId, callback) {
  const condition = {
    _id: observationId,
    'attachments._id': attachmentId,
  }

  observationModel(event).findOne(condition, function (err, observation) {
    const attachment = observation.attachments.find(attachment => attachment._id.toString() === attachmentId);
    callback(err, attachment);
  });
};

exports.addAttachment = function(event, observationId, attachmentId, file, callback) {
  const condition = {
    _id: observationId,
    attachments: {
      '$elemMatch': {
        _id: attachmentId,
        contentType: file.mimetype,
        name: file.originalname
      }
    }
  }

  const update = {
    'attachments.$.size': file.size,
    'attachments.$.relativePath': file.relativePath
  }

  observationModel(event).findOneAndUpdate(condition, update, {new: true}, function(err, observation) {
    if (err || !observation) return callback(err);

    const attachment = observation.attachments.find(attachment => attachment._id.toString() === attachmentId);
    callback(err, attachment);
  });
};

exports.removeAttachment = function(event, observationId, attachmentId, callback) {
  const update = { $pull: { attachments: { _id: attachmentId } } };
  observationModel(event).findByIdAndUpdate(observationId, update, callback);
};

exports.addAttachmentThumbnail = function(event, observationId, attachmentId, thumbnail, callback) {
  const condition = {_id: observationId, 'attachments._id': attachmentId};
  const update = {'$push': { 'attachments.$.thumbnails': thumbnail }};
  observationModel(event).update(condition, update, callback);
};
<commit_msg>Allow any GeoJSON property in observation model<commit_after>const mongoose = require('mongoose')
  , async = require('async')
  , Event = require('./event')
  , log = require('winston');

const Schema = mongoose.Schema;

// Collection to hold unique observation ids
const ObservationIdSchema = new Schema();
const ObservationId = mongoose.model('ObservationId', ObservationIdSchema);

const FormSchema = new Schema({
  formId: { type: Number, required: true }
},{
  strict: false
});

const PropertiesSchema = new Schema({
  timestamp: { type: Date, required: true },
  forms: [FormSchema]
},{
  strict: false
});

const StateSchema = new Schema({
  name: { type: String, required: true },
  userId: { type: Schema.Types.ObjectId, ref: 'User' }
});

const ThumbnailSchema = new Schema({
  contentType: { type: String, required: false },
  size: { type: Number, required: false },
  name: { type: String, required: false },
  relativePath: { type: String, required: true },
  minDimension: { type: Number, required: true },
  width: { type: Number, required: false },
  height: { type: Number, required: false}
},{
  strict: false
});

const AttachmentSchema = new Schema({
  observationFormId: { type: Schema.Types.ObjectId, required: true },
  fieldName: { type: String, required: true },
  lastModified: {type: Date, required: false},
  contentType: { type: String, required: false },
  size: { type: Number, required: false },
  name: { type: String, required: false },
  relativePath: { type: String, required: false },
  width: { type: Number, required: false },
  height: { type: Number, required: false },
  oriented: {type: Boolean, required: true, default: false },
  thumbnails: [ThumbnailSchema]
},{
  strict: false
});

// Creates the Schema for the Attachments object
const ObservationSchema = new Schema({
  type: { type: String, enum: ['Feature'], required: true},
  lastModified: {type: Date, required: false},
  userId: {type: Schema.Types.ObjectId, ref: 'User', required: false, sparse: true},
  deviceId: {type: Schema.Types.ObjectId, required: false, sparse: true},
  geometry: Schema.Types.Mixed,
  properties: PropertiesSchema,
  attachments: [AttachmentSchema],
  states: [StateSchema],
  important: {
    userId: {type: Schema.Types.ObjectId, ref: 'User', required: false},
    timestamp: {type: Date, required: false},
    description: {type: String, required: false}
  },
  favoriteUserIds: [{type: Schema.Types.ObjectId, ref: 'User'}]
},{
  strict: false,
  timestamps: {
    createdAt: 'createdAt',
    updatedAt: 'lastModified'
  }
});

ObservationSchema.index({geometry: "2dsphere"});
ObservationSchema.index({'lastModified': 1});
ObservationSchema.index({'attachments.lastModified': 1});
ObservationSchema.index({'userId': 1});
ObservationSchema.index({'deviceId': 1});
ObservationSchema.index({'properties.timestamp': 1});
ObservationSchema.index({'states.name': 1});
ObservationSchema.index({'attachments.oriented': 1});
ObservationSchema.index({'attachments.contentType': 1});
ObservationSchema.index({'attachments.thumbnails.minDimension': 1});

function transformAttachment(attachment, observation) {
  attachment.id = attachment._id;
  delete attachment._id;
  delete attachment.thumbnails;

  if (attachment.relativePath) {
    attachment.url = [observation.url, "attachments", attachment.id].join("/");
  }

  return attachment;
}

function transformState(state, observation) {
  state.id = state._id;
  delete state._id;

  state.url = [observation.url, "states", state.id].join("/");
  return state;
}

function transform(observation, ret, options) {
  if ('function' !== typeof observation.ownerDocument) {
    ret.id = ret._id;
    delete ret._id;
    delete ret.__v;

    if (options.event) {
      ret.eventId = options.event._id;
    }

    const path = options.path ? options.path : "";
    ret.url = [path, observation.id].join("/");

    if (observation.states && observation.states.length) {
      ret.state = transformState(ret.states[0], ret);
      delete ret.states;
    }

    if (observation.properties && observation.properties.forms) {
      ret.properties.forms = ret.properties.forms.map(observationForm => {
        observationForm.id = observationForm._id;
        delete observationForm._id;
        return observationForm;
      });
    }

    if (observation.attachments) {
      ret.attachments = ret.attachments.map(function (attachment) {
        return transformAttachment(attachment, ret);
      });
    }

    const populatedUserId = observation.populated('userId');
    if (populatedUserId) {
      ret.user = ret.userId;
      // TODO Update mobile clients to handle observation.userId or observation.user.id
      // Leave userId as mobile clients depend on it for observation create/update,
      ret.userId =  populatedUserId;
    }

    const populatedImportantUserId = observation.populated('important.userId')
    if (populatedImportantUserId && ret.important) {
      ret.important.user = ret.important.userId
      delete ret.important.userId;
    }
  }
}

ObservationIdSchema.set("toJSON", {
  transform: transform
});

ObservationSchema.set('toJSON', {
  transform: transform
});

ObservationSchema.set('toObject', {
  transform: transform
});

const models = {};
mongoose.model('Attachment', AttachmentSchema);
mongoose.model('Thumbnail', ThumbnailSchema);
mongoose.model('State', StateSchema);

// return a string for each property
function convertFieldForQuery(field, keys, fields) {
  keys = keys || [];
  fields = fields || {};

  for (const childField in field) {
    keys.push(childField);
    if (Object(field[childField]) === field[childField]) {
      convertFieldForQuery(field[childField], keys, fields);
    } else {
      const key = keys.join(".");
      if (field[childField]) {
        fields[key] = field[childField];
      }
      keys.pop();
    }
  }

  return fields;
}

function parseFields(fields) {
  if (fields) {
    const state = fields.state ? true : false;
    delete fields.state;

    fields = convertFieldForQuery(fields);
    if (fields.id === undefined) fields.id = true; // default is to return id if not specified
    if (fields.type === undefined) fields.type = true; // default is to return type if not specified

    if (state) {
      fields.states = {$slice: 1};
    }

    return fields;
  } else {
    return { states: {$slice: 1}};
  }
}

function observationModel(event) {
  const name = event.collectionName;
  let model = models[name];
  if (!model) {
    // Creates the Model for the Observation Schema
    model = mongoose.model(name, ObservationSchema, name);
    models[name] = model;
  }

  return model;
}

exports.observationModel = observationModel;

exports.getObservations = function(event, o, callback) {
  const conditions = {};

  const filter = o.filter || {};
  // Filter by geometry
  if (filter.geometry) {
    conditions.geometry = {
      $geoIntersects: {
        $geometry: filter.geometry
      }
    };
  }

  if (filter.startDate || filter.endDate) {
    conditions.lastModified = {};
    if (filter.startDate) {
      conditions.lastModified.$gte = filter.startDate;
    }

    if (filter.endDate) {
      conditions.lastModified.$lt = filter.endDate;
    }
  }

  if (filter.observationStartDate || filter.observationEndDate) {
    conditions['properties.timestamp'] = {};
    if (filter.observationStartDate) {
      conditions['properties.timestamp'].$gte = filter.observationStartDate;
    }

    if (filter.observationEndDate) {
      conditions['properties.timestamp'].$lt = filter.observationEndDate;
    }
  }

  if (filter.states) {
    conditions['states.0.name'] = { $in: filter.states };
  }

  if (filter.favorites && filter.favorites.userId) {
    conditions['favoriteUserIds'] = { $in: [filter.favorites.userId] };
  }

  if (filter.important) {
    conditions['important'] = {$exists: true};
  }

  const options = {};
  if (o.sort) {
    options.sort = o.sort;
  }

  const fields = parseFields(o.fields);
  if (filter.attachments === true) {
    fields.attachments = {$slice: 0};
  }

  let query = observationModel(event).find(conditions, fields, options);

  if (o.populate) {
    query = query
      .populate({
        path: 'userId',
        select: 'displayName' })
      .populate({
        path: 'important.userId',
        select: 'displayName'
      });
  }

  query.exec(callback);
};

exports.createObservationId = function(callback) {
  ObservationId.create({}, callback);
};

exports.getObservationId = function(id, callback) {
  ObservationId.findById(id, function(err, doc) {
    callback(err, doc ? doc._id : null);
  });
};

exports.getLatestObservation = function(event, callback) {
  observationModel(event).findOne({}, {lastModified: true}, {sort: {"lastModified": -1}, limit: 1}, callback);
};

exports.getObservationById = function(event, observationId, options, callback) {
  const fields = parseFields(options.fields);

  observationModel(event).findById(observationId, fields, callback);
};

exports.updateObservation = function(event, observationId, update, callback) {
  let addAttachments = [];
  let removeAttachments = [];
  update.properties.forms
    .map(observationForm => {
      observationForm._id = observationForm._id || mongoose.Types.ObjectId();
      delete observationForm.id;
      return observationForm;
    })
    .forEach(observationForm => {
      const formDefinition = event.forms.find(form => form._id === observationForm.formId);
      Object.keys(observationForm).forEach(fieldName => {
        const fieldDefinition = formDefinition.fields.find(field => field.name === fieldName);
        if (fieldDefinition && fieldDefinition.type === 'attachment') {
          const attachments = observationForm[fieldName] || [];
          addAttachments = addAttachments.concat(attachments
            .filter(attachment => attachment.action === 'add')
            .map(attachment => {
              return {
                observationFormId: observationForm._id,
                fieldName: fieldName,
                name: attachment.name,
                contentType: attachment.contentType
              }
            }));

          removeAttachments = removeAttachments.concat(attachments
            .filter(attachment => attachment.action === 'delete')
            .map(attachment => attachment.id )
          );

          delete observationForm[fieldName]
        }
      });
  });

  const ObservationModel = observationModel(event);
  ObservationModel.findById(observationId)
    .then(observation => {
      if (!observation) {
        observation = new ObservationModel({
          _id: observationId,
          userId: update.userId,
          deviceId: update.deviceId,
          states: [{ name: 'active', userId: update.userId }]
        });
      }

      observation.type = update.type;
      observation.geometry = update.geometry;
      observation.properties = update.properties;
      observation.attachments = observation.attachments.concat(addAttachments);
      observation.attachments = observation.attachments.filter(attachment => {
        return !removeAttachments.includes(attachment._id.toString())
      });

      return observation.save();
    })
    .then(observation => {
      return observation
        .populate({ path: 'userId', select: 'displayName' })
        .populate({ path: 'important.userId', select: 'displayName' })
        .execPopulate();
    })
    .then(observation => {
      callback(null, observation);
    })
    .catch(err => callback(err));
};

exports.removeObservation = function(event, observationId, callback) {
  observationModel(event).findByIdAndRemove(observationId, callback);
};

exports.removeUser = function(user, callback) {
  const condition = { userId: user._id };
  const update = { '$unset': { userId: true } };
  const options = { multi: true };

  Event.getEvents({}, function(err, events) {
    async.each(events, function(event, done) {
      observationModel(event).update(condition, update, options, function(err, numberAffected) {
        log.info('Remove deleted user from ' + numberAffected + ' documents for event ' + event.name);
        done();
      });
    },
    function(err){
      callback(err);
    });
  });
};

exports.removeDevice = function(device, callback) {
  const condition = { deviceId: device._id };
  const update = { '$unset': { deviceId: true } };
  const options = { multi: true };

  Event.getEvents(function(err, events) {
    async.each(events, function(event, done) {
      observationModel(event).update(condition, update, options, function(err, numberAffected) {
        log.info('Remove deleted device from ' + numberAffected + ' documents for event ' + event.name);
        done();
      });
    },
    function(err){
      callback(err);
    });
  });
};

exports.addState = function(event, id, state, callback) {
  const condition = { _id: mongoose.Types.ObjectId(id), 'states.0.name': { '$ne': state.name } };
  state._id = mongoose.Types.ObjectId();
  const update = {
    '$push': {
      states: {
        '$each': [state],
        '$position': 0
      }
    }
  };

  observationModel(event).update(condition, update, {upsert: false}, function(err) {
    callback(err, state);
  });
};

exports.addFavorite = function(event, observationId, user, callback) {
  const update = {
    $addToSet: {
      favoriteUserIds: user._id
    }
  };

  observationModel(event).findByIdAndUpdate(observationId, update, {new: true}, callback);
};

exports.removeFavorite = function (event, observationId, user, callback) {
  const update = {
    $pull: {
      favoriteUserIds: user._id
    }
  };

  observationModel(event).findByIdAndUpdate(observationId, update, {new: true}, callback);
};

exports.addImportant = function(event, observationId, important, callback) {
  const update = { important};
  observationModel(event).findByIdAndUpdate(observationId, update, { new: true }, callback);
}

exports.removeImportant = function(event, id, callback) {
  const update = {
    '$unset': {
      important: 1
    }
  };

  observationModel(event).findByIdAndUpdate(id, update, {new: true}, callback);
};

exports.getAttachment = function(event, observationId, attachmentId, callback) {
  const condition = {
    _id: observationId,
    'attachments._id': attachmentId,
  }

  observationModel(event).findOne(condition, function (err, observation) {
    const attachment = observation.attachments.find(attachment => attachment._id.toString() === attachmentId);
    callback(err, attachment);
  });
};

exports.addAttachment = function(event, observationId, attachmentId, file, callback) {
  const condition = {
    _id: observationId,
    attachments: {
      '$elemMatch': {
        _id: attachmentId,
        contentType: file.mimetype,
        name: file.originalname
      }
    }
  }

  const update = {
    'attachments.$.size': file.size,
    'attachments.$.relativePath': file.relativePath
  }

  observationModel(event).findOneAndUpdate(condition, update, {new: true}, function(err, observation) {
    if (err || !observation) return callback(err);

    const attachment = observation.attachments.find(attachment => attachment._id.toString() === attachmentId);
    callback(err, attachment);
  });
};

exports.removeAttachment = function(event, observationId, attachmentId, callback) {
  const update = { $pull: { attachments: { _id: attachmentId } } };
  observationModel(event).findByIdAndUpdate(observationId, update, callback);
};

exports.addAttachmentThumbnail = function(event, observationId, attachmentId, thumbnail, callback) {
  const condition = {_id: observationId, 'attachments._id': attachmentId};
  const update = {'$push': { 'attachments.$.thumbnails': thumbnail }};
  observationModel(event).update(condition, update, callback);
};
<|endoftext|>


<commit_before>'use stric';
var fs = require('fs');

module.exports = rulezEngine;

function rulezEngine() {
    this.ruleset = {"name":null, "instance":null};
    this.setRuleSet = function(ruleset){
        this.ruleset.name = ruleset;
        path  = "ruleset/"+this.ruleset.name+".js";
        if (fs.existsSync(path)){
            this.ruleset.instance = require(path);
            return true;
        }
        return false;
    };
    this.getRuleSet = function(){
        return this.ruleset.instance;
    };

    this.run = function(data)
    {
        rules = (this.ruleset.instance[this.ruleset.name].rules);
        returnType = (this.ruleset.instance[this.ruleset.name].returnType);
        results = [];
        for(i=0; i< rules.length; i++){
            if(typeof(rules[i])=='function'){
                results.push ( (rules[i])(data));
            }else{
                results.push( eval(rules[i]) );
            }
        }
         if(typeof(returnType)=='function'){
             return  ( (returnType)(results));
        }else if (returnType == "boolean"){
            return this.returnBoolean(results);
        } else if(returnType == "ResponseData"){
            return results;
        }
        return null;
    };
    this.returnBoolean = function (result){
        returnValue = true;
        for(r in result){
            returnValue = returnValue & result[r];
        }
        return returnValue;
    }
};<commit_msg>add returnType text based in a  array join<commit_after>'use stric';
var fs = require('fs');

module.exports = rulezEngine;

function rulezEngine() {
    this.ruleset = {"name":null, "instance":null};
    this.setRuleSet = function(ruleset){
        this.ruleset.name = ruleset;
        path  = "ruleset/"+this.ruleset.name+".js";
        if (fs.existsSync(path)){
            this.ruleset.instance = require(path);
            return true;
        }
        return false;
    };
    this.getRuleSet = function(){
        return this.ruleset.instance;
    };

    this.run = function(data)
    {
        rules = (this.ruleset.instance[this.ruleset.name].rules);
        returnType = (this.ruleset.instance[this.ruleset.name].returnType);
        results = [];
        for(i=0; i< rules.length; i++){
            if(typeof(rules[i])=='function'){
                results.push ( (rules[i])(data));
            }else{
                results.push( eval(rules[i]) );
            }
        }
        if(returnType=='text'){
            return results.join();
       }else if(typeof(returnType)=='function'){
             return  ( (returnType)(results));
        }else if (returnType == "boolean"){
            return this.returnBoolean(results);
        } else if(returnType == "ResponseData"){
            return results;
        }
        return null;
    };
    this.returnBoolean = function (result){
        returnValue = true;
        for(r in result){
            returnValue = returnValue & result[r];
        }
        return returnValue;
    }
};<|endoftext|>


<commit_before>/*! @license ©2013 Ruben Verborgh - Multimedia Lab / iMinds / Ghent University */

/** LinkedDataFragmentsServer is an HTTP server that provides access to Linked Data Fragments */

var path = require('path'),
    fs = require('fs'),
    http = require('http'),
    url = require('url'),
    _ = require('lodash'),
    N3Util = require('n3').Util;

// Default routes for all Linked Data Fragments servers
var defaultRoutes = {
  respondWithFragment: /^\/([\-a-z0-9_]*)$/i,
  sendStyle: '/styles/([a-z]+)',
  sendImage: '/images/([a-z]+)',
  sendIcon: '/([a-z]+).ico',
};

// Writers of fragments per type
var fragmentWriters = {
  'text/html': require('./HtmlFragmentWriter'),
  'text/turtle': require('./TurtleFragmentWriter'),
  'application/json': require('./JsonLdFragmentWriter'),
  'application/ld+json': require('./JsonLdFragmentWriter'),
};

var assetsFolder = path.join(__dirname, '../views/');

// Creates a new LinkedDataFragmentsServer for the specified data sources (key/value object)
function LinkedDataFragmentsServer(baseURL, datasources, options) {
  // Create the HTTP server
  var server = http.createServer(), sockets = 0;
  for (var member in prototype)
    server[member] = prototype[member];
  server._datasources = datasources || [];
  server._itemsPerPage = 100;
  server._sockets = {};
  server._options = options = _.clone(options);
  server._prefixes = _.clone(options.prefixes);
  server._dererefencePaths = _.clone(options.dereference);

  // Set up routing
  var routes = server._routes = _.clone(defaultRoutes);
  if (!_.isEmpty(server._dererefencePaths))
    routes.dereference = new RegExp('^(' + Object.keys(server._dererefencePaths)
                                                 .map(escapeForRegExp).join('|') + ')');

  // Attach event listeners
  server.on('request', server.onRequest.bind(server));
  server.on('error', server.respondWithError.bind(server));
  server.on('connection', function (socket) {
    var sockedId = sockets++;
    server._sockets[sockedId] = socket;
    socket.on('close', function () { delete server._sockets[sockedId]; });
  });
  return server;
}

var prototype = LinkedDataFragmentsServer.prototype = {
  // Handles an incoming HTTP request
  onRequest: function (req, res) {
    switch (req.method) {
      case "GET":
        break;
      // In case of HEAD and OPTIONS, don't write a body
      case "HEAD":
      case "OPTIONS":
        res.write = function () {};
        res.end = res.end.bind(res, '', '');
        break;
      // Only accept read-only methods
      default:
        return res.writeHead(405), res.end();
    }

    // Parse URL and redirect to the canonical URL if necessary
    var urlParts = url.parse(req.url, true);
    if (req.url !== urlParts.path)
      return this._redirect(req, res, urlParts.path);
    req.query = urlParts.query;

    // Set HTTP headers
    res.setHeader('Cache-Control', 'public, max-age=3600');
    res.setHeader('Vary', 'Accept');
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('X-Powered-By', 'Linked Data Fragments Server');

    // Execute the correct action
    for (var action in this._routes) {
      var path = this._routes[action], match = urlParts.pathname.match(path);
      if (match) {
        req.params = match.splice(1);
        try { return this[action](req, res); }
        catch (error) { return this.respondWithError(req, res, error); }
      }
    }

    // If no action found, send 404
    this.respondNotFound(req, res);
  },

  // Shows results of a data source query
  respondWithFragment: function (req, res) {
    // Find the corresponding data source
    var datasourceName = req.params[0] || 'index',
        datasource = this._datasources[datasourceName];
    if (!datasource)
      return this.respondNotFound(req, res);

    // Parse the query
    var query = req.query,
        pattern = { subject: parseEntity(query.subject, this._prefixes),
                    predicate: parseEntity(query.predicate, this._prefixes),
                    object: parseEntity(query.object, this._prefixes) },
        page = /^[1-9][0-9]*$/.test(query.page) ? parseInt(query.page, 10) : 1,
        limit = this._itemsPerPage, offset = limit * (page - 1);

    // Create the output writer
    var self = this, host = req.headers.host,
        prefixes = _.extend(_.clone(this._prefixes), { '': formatURL(host, '/') }),
        writer = this._createFragmentWriter(req, res, { query: query, type: datasource.type,
          dataset: datasourceName, datasetURL: formatURL(host, datasourceName) + '#dataset',
          pattern: pattern, fragmentURL: formatURL(host, datasourceName, query),
          prefixes: prefixes, page: page, limit: limit, offset: offset,
        }),
        metadataWritten = false, triplesEnded = false;

    // Writes a triple to the output
    function writeTriple(triple) {
      writer.writeTriple(triple);
    }

    // Writes metadata to the output
    function writeMetadata(totalCount) {
      if (!metadataWritten) {
        writer.writeMetadata(totalCount);
        metadataWritten = true;
        triplesEnded && end();
      }
    }

    // Ends the output
    function end(error) {
      triplesEnded = true;
      if (error) {
        self.respondWithError(req, res, error);
      }
      else {
        if (metadataWritten)
          writer.end(function (error) { error && self.respondWithError(req, res, error); });
        else
          setTimeout(function () { writeMetadata(Math.pow(2, 32)); }, 5000);
      }
    }

    // Send the results from the datasource to the output
    datasource.query(pattern, offset, limit, writeTriple, writeMetadata, end);
  },

  // Dereferences a URL by showing its subject fragment of a certain dataset
  dereference: function (req, res) {
    var entity = formatURL(req.headers.host, req.url),
        dataset = this._dererefencePaths[req.params[0]],
        destination = formatURL(req.headers.host, '/' + dataset, { subject: entity });
    res.writeHead(303, { 'Location': destination });
    res.end();
  },

  // Sends a stylesheet
  sendStyle: function (req, res) {
    this.sendFile(['styles', req.params[0] + '.css'], 'text/css; charset=utf-8', res);
  },

  // Sends an image
  sendImage: function (req, res) {
    this.sendFile(['images', req.params[0] + '.svg'], 'image/svg+xml', res);
  },

  // Sends an icon
  sendIcon: function (req, res) {
    this.sendFile([req.params[0] + '.ico'], 'image/x-icon', res);
  },

  // Sends a file
  sendFile: function (pathParts, mimeType, res) {
    var fileName = path.join.apply(path, [assetsFolder].concat(pathParts));
    var fileStream = fs.createReadStream(fileName);
    fileStream.on('error', function () {
      try { res.writeHead(404); }
      finally { res.end(); }
    });
    res.setHeader('Content-Type', mimeType);
    fileStream.pipe(res);
  },

  // Responds to request for an unknown URL
  respondNotFound: function (req, res) {
    var datasets = _.mapValues(this._datasources, function (name, key) {
          return formatURL(req.headers.host, key);
        }),
        options = { url: req.url, datasets: datasets },
        writer = this._createFragmentWriter(req, res, options);
    res.writeHead(404);
    writer.writeNotFound();
    writer.end();
  },

  // Responds to an application error
  respondWithError: function (req, res, error) {
    // Halt if only an error is given
    if (!res) {
      error = req, res = res = null;
      console.log(error.toString());
      return process.exit(-1);
    }

    // Ensure errors are not handled recursively
    if (req._handlingError)
      throw new Error('Already handling an error.');
    req._handlingError = true;
    // Try to render error response
    if (!res.headersSent) {
      var writer = this._createFragmentWriter(req, res, { error: error });
      res.writeHead(500);
      writer.writeError();
      writer.end();
    }
    // Try closing the response stream
    else {
      try { res.end(); }
      catch (endError) {}
    }
    // Log the error
    console.error(error.stack);
  },

  // Redirects the response
  _redirect: function (req, res, location) {
    res.writeHead(301, { location: location });
    res.end('Available at ' + location + '\n');
  },

  // Creates a fragment writer with the correct content type
  _createFragmentWriter: function (req, res, options) {
    var defaults = { prefixes: this._prefixes,
                     rootUrl: formatURL(req.headers.host, '/'),
                     title: this._options.title },
        contentType = this._negotiate(req, res, fragmentWriters),
        Writer = fragmentWriters[contentType],
        writer = new Writer(res, _.defaults(options, defaults));
    res.setHeader('Content-Type', contentType + ';charset=utf-8');
    return writer;
  },

  // Picks the content type of the options that best fits the client's preferences
  _negotiate: function (req, res, options) {
    // Collect the allowed types
    var allowedTypes = { '*/*': true };
    for (var type in options)
      allowedTypes[type] = true;

    // Parse the Accept header
    var acceptHeader = req.headers.accept || '*/*',
        accepts = acceptHeader.split(',').map(function (optionString) {
          var option = {}, settings = optionString.split(';'), match;
          // Parse key-value settings for each option
          for (var i = 1; i < settings.length; i++)
            if (match = settings[i].match(/^([^=]+)=(.+)$/))
              option[match[1]] = match[2];
          option.type = settings[0];
          return option;
        })
        // Keep only options that are acceptable (= supported and quality > 0)
        .filter(function (option) {
          if (option.type in allowedTypes) {
            if ('q' in option && /^\d+(?:\.\d+)?$/.test(option.q))
              option.q = Math.min(Math.max(parseFloat(option.q), 0.0), 1.0);
            else
              option.q = 1.0;
            if (option.q > 0.0)
              return true;
            delete allowedTypes[option.type];
          }
        })
        // Sort by descending quality values
        .sort(function (a, b) { return b.q - a.q; }),
        bestType = accepts.length ? accepts[0].type : (('*/*' in allowedTypes) && '*/*');

    // Return the best type
    if (bestType in options)
      return bestType;
    // Return the best remaining option if any type is allowed
    else if (bestType === '*/*') {
      delete allowedTypes['*/*'];
      var allowedList = Object.keys(allowedTypes);
      if (allowedList.length > 0)
        return allowedList[0];
    }

    // If no matching type was found, respond with 406
    res.writeHead(406);
    res.end('Not acceptable.\n');
    throw new Error('Not acceptable: ' + acceptHeader);
  },

  // Stops the server
  stop: function () {
    // Close data sources
    for (var datasourceName in this._datasources) {
      try { this._datasources[datasourceName].close(); }
      catch (error) { }
    }
    // Don't accept new connections, and close existing ones
    this.close();
    for (var id in this._sockets)
      this._sockets[id].destroy();
  },
};

// Parses an entity (URI, literal, or blank) into canonical form
function parseEntity(entity, prefixes) {
  // falsy, variable, or blank indicates an unknown
  if (!entity || /^\?|^_:/.test(entity))
    return null;
  // angular brackets indicate a URI
  var match = /^<(.*)>$/.exec(entity);
  if (match)
    return match[1];
  // literals don't require extra parsing
  if (N3Util.isLiteral(entity))
    return entity;
  // if it's a QName, expand it
  if (N3Util.isQName(entity))
    try { return N3Util.expandQName(entity, prefixes); }
    catch (error) { /* prefix not found */ }
  // assume it is a URI without angular brackets
  return entity;
}

// Formats a URL with the given components
function formatURL(host, path, query) {
  return url.format({ protocol: 'http', host: host, pathname: path, query: query });
}

// Escapes a string for use in a regular expression
function escapeForRegExp(string) {
  return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}

module.exports = LinkedDataFragmentsServer;
<commit_msg>Simplify fragment writing code.<commit_after>/*! @license ©2013 Ruben Verborgh - Multimedia Lab / iMinds / Ghent University */

/** LinkedDataFragmentsServer is an HTTP server that provides access to Linked Data Fragments */

var path = require('path'),
    fs = require('fs'),
    http = require('http'),
    url = require('url'),
    _ = require('lodash'),
    N3Util = require('n3').Util;

// Default routes for all Linked Data Fragments servers
var defaultRoutes = {
  respondWithFragment: /^\/([\-a-z0-9_]*)$/i,
  sendStyle: '/styles/([a-z]+)',
  sendImage: '/images/([a-z]+)',
  sendIcon: '/([a-z]+).ico',
};

// Writers of fragments per type
var fragmentWriters = {
  'text/html': require('./HtmlFragmentWriter'),
  'text/turtle': require('./TurtleFragmentWriter'),
  'application/json': require('./JsonLdFragmentWriter'),
  'application/ld+json': require('./JsonLdFragmentWriter'),
};

var assetsFolder = path.join(__dirname, '../views/');

// Creates a new LinkedDataFragmentsServer for the specified data sources (key/value object)
function LinkedDataFragmentsServer(baseURL, datasources, options) {
  // Create the HTTP server
  var server = http.createServer(), sockets = 0;
  for (var member in prototype)
    server[member] = prototype[member];
  server._datasources = datasources || [];
  server._itemsPerPage = 100;
  server._sockets = {};
  server._options = options = _.clone(options);
  server._prefixes = _.clone(options.prefixes);
  server._dererefencePaths = _.clone(options.dereference);

  // Set up routing
  var routes = server._routes = _.clone(defaultRoutes);
  if (!_.isEmpty(server._dererefencePaths))
    routes.dereference = new RegExp('^(' + Object.keys(server._dererefencePaths)
                                                 .map(escapeForRegExp).join('|') + ')');

  // Attach event listeners
  server.on('request', server.onRequest.bind(server));
  server.on('error', server.respondWithError.bind(server));
  server.on('connection', function (socket) {
    var sockedId = sockets++;
    server._sockets[sockedId] = socket;
    socket.on('close', function () { delete server._sockets[sockedId]; });
  });
  return server;
}

var prototype = LinkedDataFragmentsServer.prototype = {
  // Handles an incoming HTTP request
  onRequest: function (req, res) {
    switch (req.method) {
      case "GET":
        break;
      // In case of HEAD and OPTIONS, don't write a body
      case "HEAD":
      case "OPTIONS":
        res.write = function () {};
        res.end = res.end.bind(res, '', '');
        break;
      // Only accept read-only methods
      default:
        return res.writeHead(405), res.end();
    }

    // Parse URL and redirect to the canonical URL if necessary
    var urlParts = url.parse(req.url, true);
    if (req.url !== urlParts.path)
      return this._redirect(req, res, urlParts.path);
    req.query = urlParts.query;

    // Set HTTP headers
    res.setHeader('Cache-Control', 'public, max-age=3600');
    res.setHeader('Vary', 'Accept');
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('X-Powered-By', 'Linked Data Fragments Server');

    // Execute the correct action
    for (var action in this._routes) {
      var path = this._routes[action], match = urlParts.pathname.match(path);
      if (match) {
        req.params = match.splice(1);
        try { return this[action](req, res); }
        catch (error) { return this.respondWithError(req, res, error); }
      }
    }

    // If no action found, send 404
    this.respondNotFound(req, res);
  },

  // Shows results of a data source query
  respondWithFragment: function (req, res) {
    // Find the corresponding data source
    var datasourceName = req.params[0] || 'index',
        datasource = this._datasources[datasourceName];
    if (!datasource)
      return this.respondNotFound(req, res);

    // Parse the query
    var query = req.query,
        pattern = { subject: parseEntity(query.subject, this._prefixes),
                    predicate: parseEntity(query.predicate, this._prefixes),
                    object: parseEntity(query.object, this._prefixes) },
        page = /^[1-9][0-9]*$/.test(query.page) ? parseInt(query.page, 10) : 1,
        limit = this._itemsPerPage, offset = limit * (page - 1);

    // Create the output writer
    var self = this, host = req.headers.host,
        prefixes = _.extend(_.clone(this._prefixes), { '': formatURL(host, '/') }),
        writer = this._createFragmentWriter(req, res, { query: query, type: datasource.type,
          dataset: datasourceName, datasetURL: formatURL(host, datasourceName) + '#dataset',
          pattern: pattern, fragmentURL: formatURL(host, datasourceName, query),
          prefixes: prefixes, page: page, limit: limit, offset: offset,
        }),
        metadataWritten = false, triplesWritten = false;

    // Writes a triple to the output
    function writeTriple(triple) {
      writer.writeTriple(triple);
    }

    // Writes metadata to the output
    function writeMetadata(totalCount) {
      if (!metadataWritten) {
        metadataWritten = true;
        writer.writeMetadata(totalCount);
        if (triplesWritten) writeEnd();
      }
    }

    // Writes the end of the output
    function writeEnd(error) {
      triplesWritten = true;
      if (error) return self.respondWithError(req, res, error);
      if (metadataWritten) return writer.end();
      // If metadata doesn't arrive within 5 seconds, just write a high number
      return setTimeout(function () { writeMetadata(Math.pow(2, 32)); }, 5000);
    }

    // Send the results from the datasource to the output
    datasource.query(pattern, offset, limit, writeTriple, writeMetadata, writeEnd);
  },

  // Dereferences a URL by showing its subject fragment of a certain dataset
  dereference: function (req, res) {
    var entity = formatURL(req.headers.host, req.url),
        dataset = this._dererefencePaths[req.params[0]],
        destination = formatURL(req.headers.host, '/' + dataset, { subject: entity });
    res.writeHead(303, { 'Location': destination });
    res.end();
  },

  // Sends a stylesheet
  sendStyle: function (req, res) {
    this.sendFile(['styles', req.params[0] + '.css'], 'text/css; charset=utf-8', res);
  },

  // Sends an image
  sendImage: function (req, res) {
    this.sendFile(['images', req.params[0] + '.svg'], 'image/svg+xml', res);
  },

  // Sends an icon
  sendIcon: function (req, res) {
    this.sendFile([req.params[0] + '.ico'], 'image/x-icon', res);
  },

  // Sends a file
  sendFile: function (pathParts, mimeType, res) {
    var fileName = path.join.apply(path, [assetsFolder].concat(pathParts));
    var fileStream = fs.createReadStream(fileName);
    fileStream.on('error', function () {
      try { res.writeHead(404); }
      finally { res.end(); }
    });
    res.setHeader('Content-Type', mimeType);
    fileStream.pipe(res);
  },

  // Responds to request for an unknown URL
  respondNotFound: function (req, res) {
    var datasets = _.mapValues(this._datasources, function (name, key) {
          return formatURL(req.headers.host, key);
        }),
        options = { url: req.url, datasets: datasets },
        writer = this._createFragmentWriter(req, res, options);
    res.writeHead(404);
    writer.writeNotFound();
    writer.end();
  },

  // Responds to an application error
  respondWithError: function (req, res, error) {
    // Halt if only an error is given
    if (!res) {
      error = req, res = res = null;
      console.log(error.toString());
      return process.exit(-1);
    }

    // Ensure errors are not handled recursively
    if (req._handlingError)
      throw new Error('Already handling an error.');
    req._handlingError = true;
    // Try to render error response
    if (!res.headersSent) {
      var writer = this._createFragmentWriter(req, res, { error: error });
      res.writeHead(500);
      writer.writeError();
      writer.end();
    }
    // Try closing the response stream
    else {
      try { res.end(); }
      catch (endError) {}
    }
    // Log the error
    console.error(error.stack);
  },

  // Redirects the response
  _redirect: function (req, res, location) {
    res.writeHead(301, { location: location });
    res.end('Available at ' + location + '\n');
  },

  // Creates a fragment writer with the correct content type
  _createFragmentWriter: function (req, res, options) {
    var defaults = { prefixes: this._prefixes,
                     rootUrl: formatURL(req.headers.host, '/'),
                     title: this._options.title },
        contentType = this._negotiate(req, res, fragmentWriters),
        Writer = fragmentWriters[contentType],
        writer = new Writer(res, _.defaults(options, defaults));
    res.setHeader('Content-Type', contentType + ';charset=utf-8');
    return writer;
  },

  // Picks the content type of the options that best fits the client's preferences
  _negotiate: function (req, res, options) {
    // Collect the allowed types
    var allowedTypes = { '*/*': true };
    for (var type in options)
      allowedTypes[type] = true;

    // Parse the Accept header
    var acceptHeader = req.headers.accept || '*/*',
        accepts = acceptHeader.split(',').map(function (optionString) {
          var option = {}, settings = optionString.split(';'), match;
          // Parse key-value settings for each option
          for (var i = 1; i < settings.length; i++)
            if (match = settings[i].match(/^([^=]+)=(.+)$/))
              option[match[1]] = match[2];
          option.type = settings[0];
          return option;
        })
        // Keep only options that are acceptable (= supported and quality > 0)
        .filter(function (option) {
          if (option.type in allowedTypes) {
            if ('q' in option && /^\d+(?:\.\d+)?$/.test(option.q))
              option.q = Math.min(Math.max(parseFloat(option.q), 0.0), 1.0);
            else
              option.q = 1.0;
            if (option.q > 0.0)
              return true;
            delete allowedTypes[option.type];
          }
        })
        // Sort by descending quality values
        .sort(function (a, b) { return b.q - a.q; }),
        bestType = accepts.length ? accepts[0].type : (('*/*' in allowedTypes) && '*/*');

    // Return the best type
    if (bestType in options)
      return bestType;
    // Return the best remaining option if any type is allowed
    else if (bestType === '*/*') {
      delete allowedTypes['*/*'];
      var allowedList = Object.keys(allowedTypes);
      if (allowedList.length > 0)
        return allowedList[0];
    }

    // If no matching type was found, respond with 406
    res.writeHead(406);
    res.end('Not acceptable.\n');
    throw new Error('Not acceptable: ' + acceptHeader);
  },

  // Stops the server
  stop: function () {
    // Close data sources
    for (var datasourceName in this._datasources) {
      try { this._datasources[datasourceName].close(); }
      catch (error) { }
    }
    // Don't accept new connections, and close existing ones
    this.close();
    for (var id in this._sockets)
      this._sockets[id].destroy();
  },
};

// Parses an entity (URI, literal, or blank) into canonical form
function parseEntity(entity, prefixes) {
  // falsy, variable, or blank indicates an unknown
  if (!entity || /^\?|^_:/.test(entity))
    return null;
  // angular brackets indicate a URI
  var match = /^<(.*)>$/.exec(entity);
  if (match)
    return match[1];
  // literals don't require extra parsing
  if (N3Util.isLiteral(entity))
    return entity;
  // if it's a QName, expand it
  if (N3Util.isQName(entity))
    try { return N3Util.expandQName(entity, prefixes); }
    catch (error) { /* prefix not found */ }
  // assume it is a URI without angular brackets
  return entity;
}

// Formats a URL with the given components
function formatURL(host, path, query) {
  return url.format({ protocol: 'http', host: host, pathname: path, query: query });
}

// Escapes a string for use in a regular expression
function escapeForRegExp(string) {
  return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}

module.exports = LinkedDataFragmentsServer;
<|endoftext|>


<commit_before>'use strict';

module.exports = function(str) {
	try {
		return JSON.parse(str);
	} catch(ex) {
		// Case 1: Load for subdevices fail as they return empty values
		str = str.replace('[,]', '[null,null]');

		return JSON.parse(str);
	}
};
<commit_msg>fix JSON for aqara body sensor<commit_after>'use strict';

module.exports = function(str) {
	try {
		return JSON.parse(str);
	} catch(ex) {
		// Case 1: Load for subdevices fail as they return empty values
		str = str.replace('[,]', '[null,null]');
		// for aqara body sensor (lumi.motion.aq2)
		str = str.replace('[,,]', '[null,null,null]');

		return JSON.parse(str);
	}
};
<|endoftext|>