<commit_before>import { inject as service } from '@ember/service';
import Component from '@ember/component';
import ViewNewEdit from 'shared/mixins/view-new-edit';
import layout from './template';
import {
  get, set, computed, setProperties, observer
} from '@ember/object';
import { next } from '@ember/runloop';
import { REGIONS } from 'shared/utils/amazon';
import { OCI_REGIONS } from 'shared/utils/oci';
import { Promise } from 'rsvp';
import { isEmpty } from '@ember/utils';

const CRED_CONFIG_CHOICES = [
  {
    name:              'amazon',
    displayName:       'Amazon',
    driver:            'amazonec2',
    configField:       'amazonec2credentialConfig',
    kontainerDriverId: 'amazonelasticcontainerservice'
  },
  {
    name:              'azure',
    displayName:       'Azure',
    driver:            'azure',
    configField:       'azurecredentialConfig',
    kontainerDriverId: 'azurekubernetesservice'
  },
  {
    name:              'digitalOcean',
    displayName:       'Digital Ocean',
    driver:            'digitalocean',
    configField:       'digitaloceancredentialConfig',
  },
  {
    name:              'google',
    displayName:       'Google',
    driver:            'google',
    configField:       'googlecredentialConfig',
    kontainerDriverId: 'googlekubernetesengine'
  },
  {
    name:              'harvester',
    displayName:       'Harvester',
    driver:            'harvester',
    configField:       'harvestercredentialConfig',
  },
  {
    name:              'linode',
    displayName:       'Linode',
    driver:            'linode',
    configField:       'linodecredentialConfig',
    kontainerDriverId: 'linodekubernetesengine'
  },
  {
    name:              'oci',
    displayName:       'OCI',
    driver:            'oci',
    configField:       'ocicredentialConfig',
  },
  {
    name:              'pnap',
    displayName:       'phoenixNAP',
    driver:            'pnap',
    configField:       'pnapcredentialConfig',
  },
  {
    name:              'vmware',
    displayName:       'VMware vSphere',
    driver:            'vmwarevsphere',
    configField:       'vmwarevspherecredentialConfig',
  },
]

export default Component.extend(ViewNewEdit, {
  globalStore:               service(),
  digitalOcean:              service(),
  linode:                    service(),
  oci:                       service(),
  intl:                      service(),
  google:                    service(),
  router:                    service(),
  layout,
  nodeConfigTemplateType:    null,
  cloudCredentialType:       null,
  model:                     null,
  cancelAdd:                 null,
  doneSavingCloudCredential: null,
  disableHeader:             false,
  validatingKeys:            false,
  region:                    null,
  sinlgeCloudKeyChoice:      null,
  regionChoices:             REGIONS,
  ociRegionChoices:          OCI_REGIONS,
  mode:                      'new',
  urlInvalid:                false,
  urlWarning:                null,
  urlError:                  null,
  gkeProjectId:              null,
  clusters:                  null,
  allNodeDrivers:            null,
  allKontainerDrivers:       null,

  init() {
    this._super(...arguments);

    let cloudCredentialType = '';
    let model               = null;

    const driverName = get(this, 'driverName') === 'aks' ? 'azure' : get(this, 'driverName');

    if (driverName) {
      let match           = CRED_CONFIG_CHOICES.findBy('driver', driverName);

      cloudCredentialType = get(match, 'name');
      model               = this.globalStore.createRecord({ type: 'cloudCredential' });
    } else {
      if (get(this, 'originalModel')) {
        let configField     = Object.keys(this.originalModel).find((key) => key.toLowerCase().includes('config'));
        let configChoice    = CRED_CONFIG_CHOICES.findBy('configField', configField);

        cloudCredentialType = get(configChoice, 'name');
        model               = this.originalModel.clone();
      } else {
        model        = this.globalStore.createRecord({ type: 'cloudCredential' });
      }
    }

    this.feetchNodeDriver();
    this.feetchKontainerDriver();

    if (driverName === 'harvester' || cloudCredentialType === 'harvester') {
      this.fetchCluster();
    }

    setProperties(this, {
      cloudCredentialType,
      model,
    });

    if (!get(this, 'originalModel')) {
      this.initCloudCredentialConfig();
    }
  },

  actions: {
    selectConfig(configType) {
      this.cleanupPreviousConfig();
      set(this, 'cloudCredentialType', configType);

      this.initCloudCredentialConfig();
    },

    updateKubeconfigYaml(value) {
      set(this, 'config.kubeconfigContent', value);
    },
  },

  cloudCredentialTypeChange: observer('cloudCredentialType', function() {
    if (get(this, 'cloudCredentialType') === 'harvester') {
      this.fetchCluster();
    }
  }),

  changeClusterId: observer('config.clusterId', function() {
    const clusterId = get(this, 'config.clusterId');

    if (!clusterId) {
      return;
    }

    const currentCluster = (get(this, 'clusters') || []).find( (C) => {
      return C.id === get(this, 'config.clusterId')
    });

    currentCluster.doAction('generateKubeconfig')
      .then((obj) => {
        set(this, 'config.kubeconfigContent', get(obj, 'config'));
      })
      .catch((err) => {
        this.get('growl').fromError('Error getting kubeconfig file', err);
      })
  }),

  clusterContent: computed('clusters', function() {
    const clusterContent = (get(this, 'clusters') || []).map((O) => {
      const value = O.id;
      const label = O.name;

      return {
        label,
        value
      }
    })

    return clusterContent;
  }),

  config: computed('cloudCredentialType', 'model.{amazonec2credentialConfig,azurecredentialConfig,digitaloceancredentialConfig,googlecredentialConfig,harvestercredentialConfig,linodecredentialConfig,ocicredentialConfig,pnapcredentialConfig,vmwarevspherecredentialConfig}', function() {
    const { model }   = this;
    const configField = this.getConfigField();

    return get(model, configField);
  }),

  configChoices: computed('driverName', 'allNodeDrivers', 'allKontainerDrivers', function() {
    if (get(this, 'driverName')) {
      // const { driverName } = this;
      const driverName = get(this, 'driverName') === 'aks' ? 'azure' : get(this, 'driverName');

      let match = CRED_CONFIG_CHOICES.findBy('driver', driverName);

      next(() => {
        setProperties(this, {
          cloudCredentialType:         get(match, 'name'),
          singleCloudKeyChoice: get(match, 'displayName'),
        });
        this.initCloudCredentialConfig();
      })

      return [match];
    } else {
      if (get(this, 'allNodeDrivers') && get(this, 'allKontainerDrivers')) {
        return (CRED_CONFIG_CHOICES.filter((N) => {
          const isActive = (get(this, 'allNodeDrivers') || []).find((O) => O.id === N.driver && O.active === true) ||  (get(this, 'allKontainerDrivers') || []).find((O) => O.id === N.kontainerDriverId && O.active === true);

          return isActive ? true : false;
        })).sortBy('displayName');
      } else {
        return CRED_CONFIG_CHOICES.sortBy('displayName');
      }
    }
  }),

  savingLabel: computed('validatingKeys', 'cloudCredentialType', function() {
    if (this.validatingKeys) {
      switch (this.cloudCredentialType) {
      case 'amazon':
      case 'digitalOcean':
      case 'linode':
      case 'azure':
      case 'google':
        return 'modalAddCloudKey.saving.validating';
      case 'oci':
      case 'pnap':
      case 'vmware':
      case 'harvester':
      default:
        return 'saveCancel.saving';
      }
    }

    return 'saveCancel.saving';
  }),

  validate() {
    if ((this.errors || []).length <= 0) {
      set(this, 'errors', []);
    }
    var ok                        = this._super(...arguments);
    let errors                    = [];
    const { cloudCredentialType } = this;

    if (cloudCredentialType === 'amazon') {
      if (!get(this, 'region')) {
        ok = false;

        errors.pushObject(this.intl.t('modalAddCloudKey.errors.region'));
      }
    }

    if (cloudCredentialType === 'azure') {
      const currentRoute = this.router.currentRoute;

      if (currentRoute?.name === 'global-admin.clusters.new.launch' && currentRoute?.params?.provider === 'azureaksv2' && isEmpty(get(this, 'config.tenantId'))) {
        ok = false;

        errors.pushObject(this.intl.t('modalAddCloudKey.errors.azureTenantId'));
      }
    }

    this.parseAndCollectErrors(errors, true);

    return ok;
  },

  willSave() {
    let ok = this._super(...arguments);

    if (!ok) {
      return ok;
    }

    const { cloudCredentialType }      = this;
    const keysThatWeCanValidate = ['amazon', 'digitalOcean', 'linode', 'oci', 'google'];
    const auth                  = {
      type:  'validate',
      token: null,
    };

    if (keysThatWeCanValidate.includes(cloudCredentialType)) {
      set(this, 'validatingKeys', true);

      if (cloudCredentialType === 'linode') {
        set(auth, 'token', get(this, 'config.token'));

        return this.linode.request(auth, 'profile').then(() => {
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.status } ${ err.statusText }`);
        });
      }

      if (cloudCredentialType === 'digitalOcean') {
        set(auth, 'token', get(this, 'config.accessToken'));

        return this.digitalOcean.request(auth, 'regions').then(() => {
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.status } ${ err.statusText }`);
        });
      }

      if (cloudCredentialType === 'amazon') {
        let authConfig = {
          accessKeyId:     this.config.accessKey,
          secretAccessKey: this.config.secretKey,
          region:          this.region,
        };
        let ec2        = new AWS.EC2(authConfig);

        return new Promise((resolve, reject) => {
          ec2.describeAccountAttributes({}, (err) => {
            if ( err ) {
              reject(err);
            }

            return resolve();
          })
        }).then(() => {
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.statusCode } ${ err.code }`);
        });
      }

      if (cloudCredentialType === 'oci') {
        let authConfig = {
          region:               this.config.region,
          tenancyOCID:          this.config.tenancyId,
          userOCID:             this.config.userId,
          fingerprint:          this.config.fingerprint,
          privateKey:           this.config.privateKeyContents,
          privateKeyPassphrase: this.config.privateKeyPassphrase,
          token:                get(this, 'config.token'),
        };

        return this.oci.request(authConfig, 'availabilityDomains').then(() => {
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.message }`);
        });
      }

      if (cloudCredentialType === 'google') {
        return this.fetchZones().then(() => {
          const auth = JSON.parse(get(this, 'config.authEncodedJson'));
          const projectId = auth?.project_id;

          set(this, 'gkeProjectId', projectId);
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.message }`);
        });
      }
    }

    set(this, 'validatingKeys', false);

    return ok;
  },

  setError(message = '') {
    const translation = this.intl.t('modalAddCloudKey.errors.validation', { status: message });

    set(this, 'validatingKeys', false);

    this.parseAndCollectErrors(translation, true);

    return false;
  },

  initCloudCredentialConfig() {
    const { model }   = this;
    const configField = this.getConfigField();

    if (configField) {
      set(model, configField, this.globalStore.createRecord({ type: configField.toLowerCase() }));
    }
  },

  doneSaving(neu) {
    const driverName = get(this, 'driverName');
    const projectId = get(this, 'gkeProjectId');

    if (driverName === 'google' && projectId) {
      set(neu, this.getConfigField(), { projectId });
      set(this, 'gkeProjectId', null)
    } else {
      // API sends back empty object which doesn't overrite the keys when the response is merged.
      // Just need to ensure that when the user loads this model again the acceess key/secret/pw is not present.
      set(neu, this.getConfigField(), {});
    }

    this.model.replaceWith(neu);

    this.doneSavingCloudCredential(neu);
  },

  cleanupPreviousConfig() {
    const { model } = this;
    const configField = this.getConfigField();

    if (configField) {
      delete model[configField];
    }
  },

  getConfigField() {
    const { cloudCredentialType, configChoices } = this;

    if (cloudCredentialType) {
      const matchType = configChoices.findBy('name', cloudCredentialType);

      return get(matchType, 'configField');
    }

    return;
  },

  parseNodeTemplateConfigType(nodeTemplate) {
    return Object.keys(nodeTemplate).find((f) => f.toLowerCase().indexOf('config') > -1);
  },

  parseAndCollectErrors() {
    throw new Error('parseAndCollectErrors action is required!');
  },

  fetchCluster() {
    get(this, 'globalStore').findAll('cluster').then( (data) => {
      const harvesterCluster = (data || []).filter((C) => {
        return C.provider === 'harvester';
      })

      set(this, 'clusters', harvesterCluster)
    });
  },

  feetchNodeDriver() {
    get(this, 'globalStore').findAll('nodeDriver').then((allNodeDrivers) => {
      set(this, 'allNodeDrivers', allNodeDrivers);
    });
  },

  feetchKontainerDriver() {
    get(this, 'globalStore').findAll('kontainerDriver').then((allKontainerDrivers) => {
      set(this, 'allKontainerDrivers', allKontainerDrivers);
    });
  },

  fetchZones() {
    let credentials = null;
    let config = null;
    let projectId = null;

    try {
      credentials = get(this, 'config.authEncodedJson');
      config = JSON.parse(credentials || '{}');
      projectId = get(config, 'project_id');
    } catch (error) {
      return Promise.reject({ message: 'Invalid JSON' });
    }

    return get(this, 'globalStore').rawRequest({
      url:    '/meta/gkeZones',
      method: 'POST',
      data:   {
        credentials,
        projectId,
      }
    }).then(() => {
      return Promise.resolve();
    }).catch((xhr) => {
      return Promise.reject({ message: xhr?.body?.error });
    });
  },
});
<commit_msg>Don't require tenantId<commit_after>import { inject as service } from '@ember/service';
import Component from '@ember/component';
import ViewNewEdit from 'shared/mixins/view-new-edit';
import layout from './template';
import {
  get, set, computed, setProperties, observer
} from '@ember/object';
import { next } from '@ember/runloop';
import { REGIONS } from 'shared/utils/amazon';
import { OCI_REGIONS } from 'shared/utils/oci';
import { Promise } from 'rsvp';

const CRED_CONFIG_CHOICES = [
  {
    name:              'amazon',
    displayName:       'Amazon',
    driver:            'amazonec2',
    configField:       'amazonec2credentialConfig',
    kontainerDriverId: 'amazonelasticcontainerservice'
  },
  {
    name:              'azure',
    displayName:       'Azure',
    driver:            'azure',
    configField:       'azurecredentialConfig',
    kontainerDriverId: 'azurekubernetesservice'
  },
  {
    name:              'digitalOcean',
    displayName:       'Digital Ocean',
    driver:            'digitalocean',
    configField:       'digitaloceancredentialConfig',
  },
  {
    name:              'google',
    displayName:       'Google',
    driver:            'google',
    configField:       'googlecredentialConfig',
    kontainerDriverId: 'googlekubernetesengine'
  },
  {
    name:              'harvester',
    displayName:       'Harvester',
    driver:            'harvester',
    configField:       'harvestercredentialConfig',
  },
  {
    name:              'linode',
    displayName:       'Linode',
    driver:            'linode',
    configField:       'linodecredentialConfig',
    kontainerDriverId: 'linodekubernetesengine'
  },
  {
    name:              'oci',
    displayName:       'OCI',
    driver:            'oci',
    configField:       'ocicredentialConfig',
  },
  {
    name:              'pnap',
    displayName:       'phoenixNAP',
    driver:            'pnap',
    configField:       'pnapcredentialConfig',
  },
  {
    name:              'vmware',
    displayName:       'VMware vSphere',
    driver:            'vmwarevsphere',
    configField:       'vmwarevspherecredentialConfig',
  },
]

export default Component.extend(ViewNewEdit, {
  globalStore:               service(),
  digitalOcean:              service(),
  linode:                    service(),
  oci:                       service(),
  intl:                      service(),
  google:                    service(),
  router:                    service(),
  layout,
  nodeConfigTemplateType:    null,
  cloudCredentialType:       null,
  model:                     null,
  cancelAdd:                 null,
  doneSavingCloudCredential: null,
  disableHeader:             false,
  validatingKeys:            false,
  region:                    null,
  sinlgeCloudKeyChoice:      null,
  regionChoices:             REGIONS,
  ociRegionChoices:          OCI_REGIONS,
  mode:                      'new',
  urlInvalid:                false,
  urlWarning:                null,
  urlError:                  null,
  gkeProjectId:              null,
  clusters:                  null,
  allNodeDrivers:            null,
  allKontainerDrivers:       null,

  init() {
    this._super(...arguments);

    let cloudCredentialType = '';
    let model               = null;

    const driverName = get(this, 'driverName') === 'aks' ? 'azure' : get(this, 'driverName');

    if (driverName) {
      let match           = CRED_CONFIG_CHOICES.findBy('driver', driverName);

      cloudCredentialType = get(match, 'name');
      model               = this.globalStore.createRecord({ type: 'cloudCredential' });
    } else {
      if (get(this, 'originalModel')) {
        let configField     = Object.keys(this.originalModel).find((key) => key.toLowerCase().includes('config'));
        let configChoice    = CRED_CONFIG_CHOICES.findBy('configField', configField);

        cloudCredentialType = get(configChoice, 'name');
        model               = this.originalModel.clone();
      } else {
        model        = this.globalStore.createRecord({ type: 'cloudCredential' });
      }
    }

    this.feetchNodeDriver();
    this.feetchKontainerDriver();

    if (driverName === 'harvester' || cloudCredentialType === 'harvester') {
      this.fetchCluster();
    }

    setProperties(this, {
      cloudCredentialType,
      model,
    });

    if (!get(this, 'originalModel')) {
      this.initCloudCredentialConfig();
    }
  },

  actions: {
    selectConfig(configType) {
      this.cleanupPreviousConfig();
      set(this, 'cloudCredentialType', configType);

      this.initCloudCredentialConfig();
    },

    updateKubeconfigYaml(value) {
      set(this, 'config.kubeconfigContent', value);
    },
  },

  cloudCredentialTypeChange: observer('cloudCredentialType', function() {
    if (get(this, 'cloudCredentialType') === 'harvester') {
      this.fetchCluster();
    }
  }),

  changeClusterId: observer('config.clusterId', function() {
    const clusterId = get(this, 'config.clusterId');

    if (!clusterId) {
      return;
    }

    const currentCluster = (get(this, 'clusters') || []).find( (C) => {
      return C.id === get(this, 'config.clusterId')
    });

    currentCluster.doAction('generateKubeconfig')
      .then((obj) => {
        set(this, 'config.kubeconfigContent', get(obj, 'config'));
      })
      .catch((err) => {
        this.get('growl').fromError('Error getting kubeconfig file', err);
      })
  }),

  clusterContent: computed('clusters', function() {
    const clusterContent = (get(this, 'clusters') || []).map((O) => {
      const value = O.id;
      const label = O.name;

      return {
        label,
        value
      }
    })

    return clusterContent;
  }),

  config: computed('cloudCredentialType', 'model.{amazonec2credentialConfig,azurecredentialConfig,digitaloceancredentialConfig,googlecredentialConfig,harvestercredentialConfig,linodecredentialConfig,ocicredentialConfig,pnapcredentialConfig,vmwarevspherecredentialConfig}', function() {
    const { model }   = this;
    const configField = this.getConfigField();

    return get(model, configField);
  }),

  configChoices: computed('driverName', 'allNodeDrivers', 'allKontainerDrivers', function() {
    if (get(this, 'driverName')) {
      // const { driverName } = this;
      const driverName = get(this, 'driverName') === 'aks' ? 'azure' : get(this, 'driverName');

      let match = CRED_CONFIG_CHOICES.findBy('driver', driverName);

      next(() => {
        setProperties(this, {
          cloudCredentialType:         get(match, 'name'),
          singleCloudKeyChoice: get(match, 'displayName'),
        });
        this.initCloudCredentialConfig();
      })

      return [match];
    } else {
      if (get(this, 'allNodeDrivers') && get(this, 'allKontainerDrivers')) {
        return (CRED_CONFIG_CHOICES.filter((N) => {
          const isActive = (get(this, 'allNodeDrivers') || []).find((O) => O.id === N.driver && O.active === true) ||  (get(this, 'allKontainerDrivers') || []).find((O) => O.id === N.kontainerDriverId && O.active === true);

          return isActive ? true : false;
        })).sortBy('displayName');
      } else {
        return CRED_CONFIG_CHOICES.sortBy('displayName');
      }
    }
  }),

  savingLabel: computed('validatingKeys', 'cloudCredentialType', function() {
    if (this.validatingKeys) {
      switch (this.cloudCredentialType) {
      case 'amazon':
      case 'digitalOcean':
      case 'linode':
      case 'azure':
      case 'google':
        return 'modalAddCloudKey.saving.validating';
      case 'oci':
      case 'pnap':
      case 'vmware':
      case 'harvester':
      default:
        return 'saveCancel.saving';
      }
    }

    return 'saveCancel.saving';
  }),

  validate() {
    if ((this.errors || []).length <= 0) {
      set(this, 'errors', []);
    }
    var ok                        = this._super(...arguments);
    let errors                    = [];
    const { cloudCredentialType } = this;

    if (cloudCredentialType === 'amazon') {
      if (!get(this, 'region')) {
        ok = false;

        errors.pushObject(this.intl.t('modalAddCloudKey.errors.region'));
      }
    }

    this.parseAndCollectErrors(errors, true);

    return ok;
  },

  willSave() {
    let ok = this._super(...arguments);

    if (!ok) {
      return ok;
    }

    const { cloudCredentialType }      = this;
    const keysThatWeCanValidate = ['amazon', 'digitalOcean', 'linode', 'oci', 'google'];
    const auth                  = {
      type:  'validate',
      token: null,
    };

    if (keysThatWeCanValidate.includes(cloudCredentialType)) {
      set(this, 'validatingKeys', true);

      if (cloudCredentialType === 'linode') {
        set(auth, 'token', get(this, 'config.token'));

        return this.linode.request(auth, 'profile').then(() => {
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.status } ${ err.statusText }`);
        });
      }

      if (cloudCredentialType === 'digitalOcean') {
        set(auth, 'token', get(this, 'config.accessToken'));

        return this.digitalOcean.request(auth, 'regions').then(() => {
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.status } ${ err.statusText }`);
        });
      }

      if (cloudCredentialType === 'amazon') {
        let authConfig = {
          accessKeyId:     this.config.accessKey,
          secretAccessKey: this.config.secretKey,
          region:          this.region,
        };
        let ec2        = new AWS.EC2(authConfig);

        return new Promise((resolve, reject) => {
          ec2.describeAccountAttributes({}, (err) => {
            if ( err ) {
              reject(err);
            }

            return resolve();
          })
        }).then(() => {
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.statusCode } ${ err.code }`);
        });
      }

      if (cloudCredentialType === 'oci') {
        let authConfig = {
          region:               this.config.region,
          tenancyOCID:          this.config.tenancyId,
          userOCID:             this.config.userId,
          fingerprint:          this.config.fingerprint,
          privateKey:           this.config.privateKeyContents,
          privateKeyPassphrase: this.config.privateKeyPassphrase,
          token:                get(this, 'config.token'),
        };

        return this.oci.request(authConfig, 'availabilityDomains').then(() => {
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.message }`);
        });
      }

      if (cloudCredentialType === 'google') {
        return this.fetchZones().then(() => {
          const auth = JSON.parse(get(this, 'config.authEncodedJson'));
          const projectId = auth?.project_id;

          set(this, 'gkeProjectId', projectId);
          set(this, 'validatingKeys', false);

          return true;
        }).catch((err) => {
          return this.setError(`${ err.message }`);
        });
      }
    }

    set(this, 'validatingKeys', false);

    return ok;
  },

  setError(message = '') {
    const translation = this.intl.t('modalAddCloudKey.errors.validation', { status: message });

    set(this, 'validatingKeys', false);

    this.parseAndCollectErrors(translation, true);

    return false;
  },

  initCloudCredentialConfig() {
    const { model }   = this;
    const configField = this.getConfigField();

    if (configField) {
      set(model, configField, this.globalStore.createRecord({ type: configField.toLowerCase() }));
    }
  },

  doneSaving(neu) {
    const driverName = get(this, 'driverName');
    const projectId = get(this, 'gkeProjectId');

    if (driverName === 'google' && projectId) {
      set(neu, this.getConfigField(), { projectId });
      set(this, 'gkeProjectId', null)
    } else {
      // API sends back empty object which doesn't overrite the keys when the response is merged.
      // Just need to ensure that when the user loads this model again the acceess key/secret/pw is not present.
      set(neu, this.getConfigField(), {});
    }

    this.model.replaceWith(neu);

    this.doneSavingCloudCredential(neu);
  },

  cleanupPreviousConfig() {
    const { model } = this;
    const configField = this.getConfigField();

    if (configField) {
      delete model[configField];
    }
  },

  getConfigField() {
    const { cloudCredentialType, configChoices } = this;

    if (cloudCredentialType) {
      const matchType = configChoices.findBy('name', cloudCredentialType);

      return get(matchType, 'configField');
    }

    return;
  },

  parseNodeTemplateConfigType(nodeTemplate) {
    return Object.keys(nodeTemplate).find((f) => f.toLowerCase().indexOf('config') > -1);
  },

  parseAndCollectErrors() {
    throw new Error('parseAndCollectErrors action is required!');
  },

  fetchCluster() {
    get(this, 'globalStore').findAll('cluster').then( (data) => {
      const harvesterCluster = (data || []).filter((C) => {
        return C.provider === 'harvester';
      })

      set(this, 'clusters', harvesterCluster)
    });
  },

  feetchNodeDriver() {
    get(this, 'globalStore').findAll('nodeDriver').then((allNodeDrivers) => {
      set(this, 'allNodeDrivers', allNodeDrivers);
    });
  },

  feetchKontainerDriver() {
    get(this, 'globalStore').findAll('kontainerDriver').then((allKontainerDrivers) => {
      set(this, 'allKontainerDrivers', allKontainerDrivers);
    });
  },

  fetchZones() {
    let credentials = null;
    let config = null;
    let projectId = null;

    try {
      credentials = get(this, 'config.authEncodedJson');
      config = JSON.parse(credentials || '{}');
      projectId = get(config, 'project_id');
    } catch (error) {
      return Promise.reject({ message: 'Invalid JSON' });
    }

    return get(this, 'globalStore').rawRequest({
      url:    '/meta/gkeZones',
      method: 'POST',
      data:   {
        credentials,
        projectId,
      }
    }).then(() => {
      return Promise.resolve();
    }).catch((xhr) => {
      return Promise.reject({ message: xhr?.body?.error });
    });
  },
});
<|endoftext|>


<commit_before>
var v0 = require('./test/styles/v0.js'),
    beautify = require('js-beautify').js_beautify;

function extend(dest, src) {
    for (var i in src) {
        dest[i] = src[i];
    }
    return dest;
}

module.exports = function (v0) {

    var v1 = {};

    v1.version = '1';

    // parse buckets

    var bucketIndex = {
        background: {id: 'background'}
    };

    for (var id in v0.buckets) {
        var v0bucket = v0.buckets[id];
        var bucket = {id: id};

        var filters = [];

        if (v0bucket.source) {
            filters.push('source == ' + v0bucket.source);
        }

        if (v0bucket.layer) {
            filters.push('layer == ' + v0bucket.layer);
        }
        if (v0bucket.value) {
            var valueFilters = (Array.isArray(v0bucket.value) ? v0bucket.value : [v0bucket.value]).map(function (value) {
                return v0bucket.field + ' == ' + value;
            });
            if (valueFilters.length > 1) {
                filters.push('(' + valueFilters.join(' || ') + ')');
            }else {
                filters.push(valueFilters.join(' || '));
            }
        }
        if (filters.length) {
            bucket.filter = filters.join(' && ');
        }

        bucketIndex[id] = bucket;
    }

    // parse structure

    function parseStructure(structure) {
        var buckets = [];

        for (var i = 0; i < structure.length; i++) {

            var id = structure[i].name,
                bucketId = structure[i].bucket,
                bucket = {};

            if (structure[i].layers) {
                bucket.layers = parseStructure(structure[i].layers);
            } else {
                extend(bucket, bucketIndex[bucketId]);
            }

            bucket.id = id;

            buckets.push(bucket);
        }

        return buckets;
    }

    v1.buckets = parseStructure(v0.structure);

    // v1.styles = v0.classes;

    console.log(beautify(JSON.stringify(v1)));

    return v1;
};

module.exports(v0);
<commit_msg>add quotes to strings in filters<commit_after>
var v0 = require('./test/styles/v0.js'),
    beautify = require('js-beautify').js_beautify;

function append(dest, src) {
    for (var i in src) {
        if (!(i in dest)) {
            dest[i] = src[i];
        }
    }
    return dest;
}

function jsonValue(value) {
    return typeof value === 'string' ? '\'' + value + '\'' : value;
}

module.exports = function (v0) {

    var v1 = {};

    v1.version = '1';

    // parse buckets

    var bucketIndex = {
        background: {id: 'background'}
    };

    for (var id in v0.buckets) {
        var v0bucket = v0.buckets[id];
        var bucket = {id: id};

        var filters = [];

        if (v0bucket.source) {
            filters.push('source == ' + jsonValue(v0bucket.source));
        }

        if (v0bucket.layer) {
            filters.push('layer == ' + jsonValue(v0bucket.layer));
        }
        if (v0bucket.value) {
            var valueFilters = (Array.isArray(v0bucket.value) ? v0bucket.value : [v0bucket.value]).map(function (value) {
                return v0bucket.field + ' == ' + jsonValue(value);
            });
            if (valueFilters.length > 1) {
                filters.push('(' + valueFilters.join(' || ') + ')');
            }else {
                filters.push(valueFilters.join(' || '));
            }
        }
        if (filters.length) {
            bucket.filter = filters.join(' && ');
        }

        bucketIndex[id] = bucket;
    }

    // parse structure

    function parseStructure(structure) {
        var buckets = [];

        for (var i = 0; i < structure.length; i++) {

            var id = structure[i].name,
                bucketId = structure[i].bucket,
                bucket = {id: id};

            if (structure[i].layers) {
                bucket.layers = parseStructure(structure[i].layers);
            } else {
                append(bucket, bucketIndex[bucketId]);
            }

            buckets.push(bucket);
        }

        return buckets;
    }

    v1.buckets = parseStructure(v0.structure);

    // v1.styles = v0.classes;

    console.log(beautify(JSON.stringify(v1)));

    return v1;
};

module.exports(v0);
<|endoftext|>


<commit_before>'use strict';

const _ = require('lodash');

const DB = require('./db');
const Config = require('./config');
const Interface = require('./interface');
const Telegram = require('./telegram');

function getCommand(command) {
    const commands = {
        '/add': {
            fn: addUser,
            help: 'add yourself as a user of knightwatcher'
        },

        '/remove': {
            fn: removeUser,
            help: 'remove yourself as a user of knightwatcher'
        },

        '/message': {
            fn: messageUser
        },

        '/start': {
            fn: displayInfo
        },

        '/help': {
            fn: displayInfo,
            help: 'display help information'
        }
    };
    if (command) {
        return commands[command] ? commands[command] : {
            fn: handleUnknownCommand,
            help: 'N/A'
        };
    }
    return commands;
}

function getDisplayMessage() {
    const message = 'Welcome to KnightWatcher! A bot for all your alert needs.\n';
    return _.reduce(getCommand(), function (prev, value, key) {
        if (value.help) {
            return `${prev}\n${key} - ${value.help}`;
        }
        return prev;
    }, message);
}

function whoami(req, res) {
    return Interface.respond(this, 200);
}

function authorize(req, res) {
    const env = Config.getEnv();
    return req.queryString.apiKey === env.TELEGRAM_API_KEY;
}

function sendUnauthorized(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const message = 'You do not have access to this resource.';
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return Telegram
        .sendMessage(chatId, message)
        .then(successResponse)
        .catch(failResponse);
}

function handleUnknownCommand(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const message = 'I don\'t know what to do with that. Please see /help for a full list of commands.';
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return Telegram
        .sendMessage(chatId, message)
        .then(successResponse)
        .catch(failResponse);
}

function displayInfo(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const message = getDisplayMessage();
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return Telegram
        .sendMessage(chatId, message)
        .then(successResponse)
        .catch(failResponse);
}

function addUser(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const params = {
        Item: _.omitBy({
            username: _.get(req.body, 'message.chat.username'),
            firstname: _.get(req.body, 'message.chat.first_name'),
            lastname: _.get(req.body, 'message.chat.last_name'),
            chatId: chatId,
            active: true
        }, _.isNil)
    };
    const sendTelegramMessage = Telegram.sendMessage.bind(
        null, chatId, 'Added user'
    );
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return DB
        .putItem(params)
        .then(sendTelegramMessage)
        .then(successResponse)
        .catch(failResponse);
}

function removeUser(req, res) {
    const params = {
        Item: {
            username: _.get(req.body, 'message.chat.username'),
            active: false
        }
    };
    const sendTelegramMessage = Telegram.sendMessage.bind(
        null, _.get(req.body, 'message.chat.id'), 'Removed user'
    );
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return DB
        .updateItem(params)
        .then(sendTelegramMessage)
        .then(successResponse)
        .catch(failResponse);
}

function messageUser(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const params1 = {
        Key: {
            username: _.get(req.body, 'message.chat.username')
        }
    };
    const handleItem = function (response) {
        const item = response.Item || {};
        return item.active ? Promise.resolve() : Promise.reject(new Error('Failed to send message'));
    };
    const text = _.get(req.body, 'message.text', '');
    const recipient = (text.split(' ')[1] || '').split(/\n/)[0] || '';
    const message = text.split(/\n/)[1] || '';
    const params2 = {
        Key: {
            username: recipient
        }
    };
    const getItem = DB.getItem.bind(null, params2);
    const sendTelegramMessage = function (response) {
        const item = response.Item || {};
        return item.active ? Telegram.sendMessage(item.chatId, message) : Promise.reject(new Error('Cannot messsage user'));
    };
    const sendSuccessMessage = Telegram.sendMessage.bind(
        null, chatId, 'Message sent.'
    );
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);
    const messageAndFailResponse = function () {
        return Telegram
            .sendMessage(chatId, 'Failed to send message.')
            .then(successResponse)
            .catch(failResponse);
    };

    if (recipient && message) {
        return DB
            .getItem(params1)
            .then(handleItem)
            .then(getItem)
            .then(sendTelegramMessage)
            .then(sendSuccessMessage)
            .then(successResponse)
            .catch(messageAndFailResponse);
    }
    return Telegram
        .sendMessage(chatId, '/message must be in the form\n\n/message <recipient>\n<message>')
        .then(successResponse)
        .catch(failResponse);
}

function processTelegram(req, res) {
    if (authorize(req, res)) {
        const command = _.get(req.body, 'message.text', '').split(' ')[0];
        return getCommand(command).fn.call(this, req, res);
    }
    return sendUnauthorized.call(this, req, res);
}

function processCircle(req, res) {
    const env = Config.getEnv();
    const payload = req.body.payload || {};
    const message = `Failed build on CircleCI\n
        repo: ${payload.reponame}
        build: ${payload.build_url}
        outcome: ${payload.outcome}
        timestamp: ${payload.stop_time}`.replace(/\t/, '');
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return Telegram
        .sendMessage(env.DEFAULT_NOTIFIER, message)
        .then(successResponse)
        .catch(failResponse);
}

function sendMessage(req, res) {
    const params = {
        Key: {
            username: req.body.username
        }
    };
    const handleItem = function (response) {
        const item = response.Item || {};
        if (item.active) {
            return Telegram
                .sendMessage(item.chatId, req.body.text);
        }
        return Promise.resolve();
    };
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return DB
        .getItem(params)
        .then(handleItem)
        .then(successResponse)
        .catch(failResponse);
}

module.exports = {
    whoami: whoami,
    processTelegram: processTelegram,
    processCircle: processCircle,
    sendMessage: sendMessage
};
<commit_msg>Added condition for sending message from /hooks/circle<commit_after>'use strict';

const _ = require('lodash');

const DB = require('./db');
const Config = require('./config');
const Interface = require('./interface');
const Telegram = require('./telegram');

function getCommand(command) {
    const commands = {
        '/add': {
            fn: addUser,
            help: 'add yourself as a user of knightwatcher'
        },

        '/remove': {
            fn: removeUser,
            help: 'remove yourself as a user of knightwatcher'
        },

        '/message': {
            fn: messageUser
        },

        '/start': {
            fn: displayInfo
        },

        '/help': {
            fn: displayInfo,
            help: 'display help information'
        }
    };
    if (command) {
        return commands[command] ? commands[command] : {
            fn: handleUnknownCommand,
            help: 'N/A'
        };
    }
    return commands;
}

function getDisplayMessage() {
    const message = 'Welcome to KnightWatcher! A bot for all your alert needs.\n';
    return _.reduce(getCommand(), function (prev, value, key) {
        if (value.help) {
            return `${prev}\n${key} - ${value.help}`;
        }
        return prev;
    }, message);
}

function whoami(req, res) {
    return Interface.respond(this, 200);
}

function authorize(req, res) {
    const env = Config.getEnv();
    return req.queryString.apiKey === env.TELEGRAM_API_KEY;
}

function sendUnauthorized(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const message = 'You do not have access to this resource.';
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return Telegram
        .sendMessage(chatId, message)
        .then(successResponse)
        .catch(failResponse);
}

function handleUnknownCommand(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const message = 'I don\'t know what to do with that. Please see /help for a full list of commands.';
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return Telegram
        .sendMessage(chatId, message)
        .then(successResponse)
        .catch(failResponse);
}

function displayInfo(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const message = getDisplayMessage();
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return Telegram
        .sendMessage(chatId, message)
        .then(successResponse)
        .catch(failResponse);
}

function addUser(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const params = {
        Item: _.omitBy({
            username: _.get(req.body, 'message.chat.username'),
            firstname: _.get(req.body, 'message.chat.first_name'),
            lastname: _.get(req.body, 'message.chat.last_name'),
            chatId: chatId,
            active: true
        }, _.isNil)
    };
    const sendTelegramMessage = Telegram.sendMessage.bind(
        null, chatId, 'Added user'
    );
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return DB
        .putItem(params)
        .then(sendTelegramMessage)
        .then(successResponse)
        .catch(failResponse);
}

function removeUser(req, res) {
    const params = {
        Item: {
            username: _.get(req.body, 'message.chat.username'),
            active: false
        }
    };
    const sendTelegramMessage = Telegram.sendMessage.bind(
        null, _.get(req.body, 'message.chat.id'), 'Removed user'
    );
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return DB
        .updateItem(params)
        .then(sendTelegramMessage)
        .then(successResponse)
        .catch(failResponse);
}

function messageUser(req, res) {
    const chatId = _.get(req.body, 'message.chat.id');
    const params1 = {
        Key: {
            username: _.get(req.body, 'message.chat.username')
        }
    };
    const handleItem = function (response) {
        const item = response.Item || {};
        return item.active ? Promise.resolve() : Promise.reject(new Error('Failed to send message'));
    };
    const text = _.get(req.body, 'message.text', '');
    const recipient = (text.split(' ')[1] || '').split(/\n/)[0] || '';
    const message = text.split(/\n/)[1] || '';
    const params2 = {
        Key: {
            username: recipient
        }
    };
    const getItem = DB.getItem.bind(null, params2);
    const sendTelegramMessage = function (response) {
        const item = response.Item || {};
        return item.active ? Telegram.sendMessage(item.chatId, message) : Promise.reject(new Error('Cannot messsage user'));
    };
    const sendSuccessMessage = Telegram.sendMessage.bind(
        null, chatId, 'Message sent.'
    );
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);
    const messageAndFailResponse = function () {
        return Telegram
            .sendMessage(chatId, 'Failed to send message.')
            .then(successResponse)
            .catch(failResponse);
    };

    if (recipient && message) {
        return DB
            .getItem(params1)
            .then(handleItem)
            .then(getItem)
            .then(sendTelegramMessage)
            .then(sendSuccessMessage)
            .then(successResponse)
            .catch(messageAndFailResponse);
    }
    return Telegram
        .sendMessage(chatId, '/message must be in the form\n\n/message <recipient>\n<message>')
        .then(successResponse)
        .catch(failResponse);
}

function processTelegram(req, res) {
    if (authorize(req, res)) {
        const command = _.get(req.body, 'message.text', '').split(' ')[0];
        return getCommand(command).fn.call(this, req, res);
    }
    return sendUnauthorized.call(this, req, res);
}

function processCircle(req, res) {
    const env = Config.getEnv();
    const payload = req.body.payload || {};
    const shouldSendMessage = ['infrastructure_fail', 'timedout', 'failed'].indexOf(payload.outcome) !== -1;

    if (shouldSendMessage) {
        const message = `Failed build on CircleCI\n
            username: ${payload.username}
            repository: ${payload.reponame}
            build: ${payload.build_url}
            outcome: ${payload.outcome}
            timestamp: ${payload.stop_time}`.replace(/ +/g, '');
        const successResponse = Interface.respond.bind(null, this, 200, {});
        const failResponse = Interface.respond.bind(null, this, 500);

        return Telegram
            .sendMessage(env.DEFAULT_NOTIFIER, message)
            .then(successResponse)
            .catch(failResponse);
    }

    return Interface.respond(this, 200);
}

function sendMessage(req, res) {
    const params = {
        Key: {
            username: req.body.username
        }
    };
    const handleItem = function (response) {
        const item = response.Item || {};
        if (item.active) {
            return Telegram
                .sendMessage(item.chatId, req.body.text);
        }
        return Promise.resolve();
    };
    const successResponse = Interface.respond.bind(null, this, 200, {});
    const failResponse = Interface.respond.bind(null, this, 500);

    return DB
        .getItem(params)
        .then(handleItem)
        .then(successResponse)
        .catch(failResponse);
}

module.exports = {
    whoami: whoami,
    processTelegram: processTelegram,
    processCircle: processCircle,
    sendMessage: sendMessage
};
<|endoftext|>


<commit_before>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
// GO AFTER THE REQUIRES BELOW.
//
//= require jquery
//= require jquery_ujs
//= require_tree .<commit_msg>add bootstrap to requirements for application.js<commit_after>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
// GO AFTER THE REQUIRES BELOW.
//
//= require jquery
//= require jquery_ujs
//= require_tree .
//= require bootstrap<|endoftext|>


<commit_before>// ========================================================================
// Copyright (c) 2008-2009, Metaweb Technologies, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials provided
//       with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES AND CONTRIBUTORS
// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL METAWEB
// TECHNOLOGIES OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
// OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
// DAMAGE.
// ========================================================================

/*
**  Automatic reconciliation
*/
var manualQueue = {};
var automaticQueue = [];

function beginAutoReconciliation() {
    $(".nowReconciling").show();
    $(".notReconciling").hide();
    $("#gettingInput").remove();
    reconciliationBegun = true;
    autoReconcile();
}

function finishedAutoReconciling() {
    $(".nowReconciling").hide();
    $('.notReconciling').show();
}

function autoReconcile() {
    if (automaticQueue.length == 0) {
        finishedAutoReconciling();
        return;
    }
    updateUnreconciledCount();
    getCandidates(automaticQueue[0], autoReconcileResults);
}

function constructReconciliationQuery(entity, typeless) {
    function constructQueryPart(value) {
        if (value.id != undefined && value.id != "" && value.id != "None")
            return {"id":value.id, "name":value["/type/object/name"]}
        if (value['/rec_ui/id'] !== undefined)
            return $.makeArray(value["/type/object/name"])[0];
        return value;
    }
    var query = {}
    var headers = entity["/rec_ui/headers"];
    for (var i = 0; i < headers.length; i++) {
        var prop = headers[i];
        if (prop.charAt(0) != "/") continue;
        var parts = prop.split(":");
        $.each($.makeArray(entity.getChainedProperty(prop)),function(j, value) {
            var slot = query;
            if (value == undefined || value == "")
                return;
            if (parts.length === 1){
                slot[prop] = slot[prop] || [];
                slot[prop][j] = constructQueryPart(value);
                return;
            }
            slot[parts[0]]    = slot[parts[0]]    || [];
            slot[parts[0]][j] = slot[parts[0]][j] || {};
            slot = slot[parts[0]][j];
            $.each(parts.slice(1,parts.length-1), function(k,part) {
                slot[part] = slot[part] || {};
                slot = slot[part];
            });
            var lastPart = parts[parts.length-1];
            slot[lastPart] = constructQueryPart(value);
        })
    }
    if (typeless || !query['/type/object/type'])
        query['/type/object/type'] = ['/common/topic'];
    return query;
}

function getCandidates(entity, callback,typeless) {
    function handler(results) {
        entity.reconResults = results;
        callback(entity);
    }
    var defaultLimit = 4;
    var limit = defaultLimit;
    if (entity.reconResults)
        limit = entity.reconResults.length * 2;
    if (!entity.typelessRecon && typeless){
        entity.typelessRecon = true;
        limit = defaultLimit;
    }
    var query = constructReconciliationQuery(entity,typeless);
    $.getJSON(reconciliation_url + "query?jsonp=?", {q:JSON.stringify(query), limit:limit}, handler);
}

function autoReconcileResults(entity) {
    automaticQueue.shift();
    // no results, set to None:
    if(entity.reconResults.length == 0) {
        if (!entity.typelessRecon)
            getCandidates(entity,autoReconcileResults,true);
        entity["id"] = "None";
        warn("No candidates found for the object:");
        warn(entity);
        addColumnRecCases(entity);
    }
    // match found:
    else if(entity.reconResults[0]["match"] == true) {
        entity.id = entity.reconResults[0].id;
        canonicalizeFreebaseId(entity);
        entity["/rec_ui/freebase_name"] = entity.reconResults[0].name;
        addColumnRecCases(entity);
    }
    else {
        var wasEmpty = isObjectEmpty(manualQueue);
        manualQueue[entity["/rec_ui/id"]] = entity;
        if (wasEmpty)
            manualReconcile();
    }
    autoReconcile();
}

/*
** Manual Reconciliation
*/

function manualReconcile() {
    if ($(".manualReconChoices:visible").length === 0) {
        var val = getFirstValue(manualQueue);
        if(val != undefined) {
            displayReconChoices(val["/rec_ui/id"])
            renderReconChoices(getSecondValue(manualQueue)); //render-ahead the next one
        }
        else{
            $(".manualQueueEmpty").show();
            $(".manualReconciliation").hide();
        }
    }
}

function displayReconChoices(entityID) {
    var entity = entities[entityID];
    if (entity === undefined) return;
    $(".manualQueueEmpty").hide();
    $(".manualReconciliation").show();
    if (! $("#manualReconcile" + entityID)[0])
        renderReconChoices(entity);
    $(".manualReconChoices:visible").remove();
    $("#manualReconcile" + entityID).show();
}

function renderReconChoices(entity) {
    if (entity == undefined) return;
    var template = $("#manualReconcileTemplate").clone();
    template[0].id = "manualReconcile" + entity['/rec_ui/id'];
    var headers = entity["/rec_ui/headers"];
    var mqlProps = entity["/rec_ui/mql_props"];

    var currentRecord = $(".recordVals",template);
    for(var i = 0; i < headers.length; i++) {
        currentRecord.append(node("label", getPropName(headers[i]) + ":", {"for":idToClass(headers[i])}));
        currentRecord.append(node("div", {"class":"propertyGroup"}).append(displayValue(entity.getChainedProperty(headers[i]))));
    }

    var tableHeader = $(".reconciliationCandidates table thead", template).empty();
    var columnHeaders = ["","Image","Name","Type"].concat($.map(mqlProps,getPropName)).concat(["Score"]);
    for (var i = 0; i < columnHeaders.length; i++)
        tableHeader.append(node("th",columnHeaders[i],{"class":"bottomHeader"}));

    var tableBody = $(".reconciliationCandidates table tbody", template).empty();
    for (var i = 0; i < entity.reconResults.length; i++){
        tableBody.append(renderCandidate(entity.reconResults[i], mqlProps, entity));
        fetchMqlProps(entity.reconResults[i], entity);
    }

    function updateCandidates() {
        $('.reconciliationCandidates table tbody tr:odd', template).addClass('odd');
        $('.reconciliationCandidates table tbody tr:even', template).addClass('even');
        numCandidates = entity.reconResults.length;
    }
    updateCandidates();
    $(".find_topic", template)
        .suggest({type:entity['/type/object/type'],
                  type_strict:"should",
                  flyout:true})
        .bind("fb-select", function(e, data) {
          entity['/rec_ui/freebase_name'] = $.makeArray(data.name);
          handleReconChoice(entity, data.id);
        });
    $(".otherSelection", template).click(function() {handleReconChoice(entity, this.name)});

    $(".moreButton",template).click(function() {
        $(".loadingMoreCandidates", template).fadeIn();
        getCandidates(entity, function() {
            $(".loadingMoreCandidates", template).hide();
            if (entity.reconResults.length <= numCandidates)
                return $(".moreButton",template).fadeOut();
            for (var i = numCandidates; i < entity.reconResults.length; i++){
                var candidate = renderCandidate(entity.reconResults[i], mqlProps, entity).appendTo(tableBody);
                fetchMqlProps(entity.reconResults[i], entity);
            }
            updateCandidates();

        });
    });
    template.insertAfter("#manualReconcileTemplate")
}

function renderCandidate(result, mqlProps, entity) {
    var url = freebase_url + "/view/" + result['id'];
    var tableRow = node("tr", {"class":idToClass(result["id"])});

    var button = node("button", "Select",
       {"class":'manualSelection',
        "name":result.id})
    tableRow.append(node("td",button));
    button.click(function(val) {entity['/rec_ui/freebase_name'] = result.name; handleReconChoice(entity, result.id)})

    node("td",
         node("img",{src:freebase_url + "/api/trans/image_thumb/"+result['id']+"?maxwidth=100&maxheight=100"})
    ).appendTo(tableRow);

    tableRow.append(node("td",displayValue(result)));
    var displayTypes = [];
    $.each($.makeArray(result.type), function(_,typeId) {
        //types that end in /topic are uninteresting
        if (typeId.match(/\/topic$/)) return;
        displayTypes.push(freebase.makeLink(typeId));
    })
    node("td").append(wrapForOverflow(displayTypes)).appendTo(tableRow);

    mqlProps = groupProperties(mqlProps).getPropsForRows();
    for(var j = 0; j < mqlProps.length; j++)
        tableRow.append(
            node("td", node("img",{src:"spinner.gif"}),
                 {"class":"replaceme "+idToClass(mqlProps[j])})
        );
    tableRow.append(node("td",result["score"]));
    return tableRow;
}

function fetchMqlProps(reconResult, entity) {
    var mqlProps = entity["/rec_ui/mql_props"];
    var query = {"id":reconResult["id"]};
    $.each(mqlProps, function(_, prop) {
        var slot = query;
        var parts = prop.split(":");
        $.each(parts.slice(0,parts.length-1), function(_, part) {
            slot[part] = slot[part] || [{optional:true}];
            slot = slot[part][0];
        });
        var lastPart = parts[parts.length-1];
        if (isValueProperty(lastPart))
            slot[lastPart] = [];
        else
            slot[lastPart] = [{"name":null,"id":null,"optional":true}];
    })
    var envelope = {query:query};
    function handler(results) {
        fillInMQLProps(entity, results);
        //don't show annoying loading symbols indefinitely if there's an error
        $("#manualReconcile" + entity["/rec_ui/id"] + " .replaceme").empty();
    }
    freebase.mqlRead(envelope,handler);
}

function fillInMQLProps(entity, mqlResult) {
    var context = $("#manualReconcile" + entity["/rec_ui/id"]);
    if (!mqlResult || mqlResult["code"] != "/api/status/ok" || mqlResult["result"] == null) {
        error(mqlResult);
        return;
    }

    var result = mqlResult.result;
    var entity = $("tr." + idToClass(result.id),context);

    for (var i = 0; i < mqlProps.length; i++) {
        var cell = $("td." + idToClass(mqlProps[i]), entity).empty();
        cell.append(displayValue(getChainedProperty(result, mqlProps[i])));
        cell.removeClass("replaceme");
    }
}

function handleReconChoice(entity,freebaseId) {
    delete manualQueue[entity["/rec_ui/id"]];
    $("#manualReconcile" + entity['/rec_ui/id']).remove();
    if (freebaseId != undefined){
        entity.id = freebaseId;
        canonicalizeFreebaseId(entity);
    }
    addColumnRecCases(entity);
    updateUnreconciledCount();
    manualReconcile();
}

function canonicalizeFreebaseId(entity) {
    var envelope = {query:{"myId:id":entity.id, "id":null}}
    freebase.mqlRead(envelope, function(results){
        if (results && results.result && results.result.id)
            entity.id = results.result.id
    });
}<commit_msg>Render-ahead more reliably<commit_after>// ========================================================================
// Copyright (c) 2008-2009, Metaweb Technologies, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials provided
//       with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES AND CONTRIBUTORS
// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL METAWEB
// TECHNOLOGIES OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
// OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
// DAMAGE.
// ========================================================================

/*
**  Automatic reconciliation
*/
var manualQueue = {};
var automaticQueue = [];

function beginAutoReconciliation() {
    $(".nowReconciling").show();
    $(".notReconciling").hide();
    $("#gettingInput").remove();
    reconciliationBegun = true;
    autoReconcile();
}

function finishedAutoReconciling() {
    $(".nowReconciling").hide();
    $('.notReconciling').show();
}

function autoReconcile() {
    if (automaticQueue.length == 0) {
        finishedAutoReconciling();
        return;
    }
    updateUnreconciledCount();
    getCandidates(automaticQueue[0], autoReconcileResults);
}

function constructReconciliationQuery(entity, typeless) {
    function constructQueryPart(value) {
        if (value.id != undefined && value.id != "" && value.id != "None")
            return {"id":value.id, "name":value["/type/object/name"]}
        if (value['/rec_ui/id'] !== undefined)
            return $.makeArray(value["/type/object/name"])[0];
        return value;
    }
    var query = {}
    var headers = entity["/rec_ui/headers"];
    for (var i = 0; i < headers.length; i++) {
        var prop = headers[i];
        if (prop.charAt(0) != "/") continue;
        var parts = prop.split(":");
        $.each($.makeArray(entity.getChainedProperty(prop)),function(j, value) {
            var slot = query;
            if (value == undefined || value == "")
                return;
            if (parts.length === 1){
                slot[prop] = slot[prop] || [];
                slot[prop][j] = constructQueryPart(value);
                return;
            }
            slot[parts[0]]    = slot[parts[0]]    || [];
            slot[parts[0]][j] = slot[parts[0]][j] || {};
            slot = slot[parts[0]][j];
            $.each(parts.slice(1,parts.length-1), function(k,part) {
                slot[part] = slot[part] || {};
                slot = slot[part];
            });
            var lastPart = parts[parts.length-1];
            slot[lastPart] = constructQueryPart(value);
        })
    }
    if (typeless || !query['/type/object/type'])
        query['/type/object/type'] = ['/common/topic'];
    return query;
}

function getCandidates(entity, callback,typeless) {
    function handler(results) {
        entity.reconResults = results;
        callback(entity);
    }
    var defaultLimit = 4;
    var limit = defaultLimit;
    if (entity.reconResults)
        limit = entity.reconResults.length * 2;
    if (!entity.typelessRecon && typeless){
        entity.typelessRecon = true;
        limit = defaultLimit;
    }
    var query = constructReconciliationQuery(entity,typeless);
    $.getJSON(reconciliation_url + "query?jsonp=?", {q:JSON.stringify(query), limit:limit}, handler);
}

function autoReconcileResults(entity) {
    automaticQueue.shift();
    // no results, set to None:
    if(entity.reconResults.length == 0) {
        if (!entity.typelessRecon)
            getCandidates(entity,autoReconcileResults,true);
        entity["id"] = "None";
        warn("No candidates found for the object:");
        warn(entity);
        addColumnRecCases(entity);
    }
    // match found:
    else if(entity.reconResults[0]["match"] == true) {
        entity.id = entity.reconResults[0].id;
        canonicalizeFreebaseId(entity);
        entity["/rec_ui/freebase_name"] = entity.reconResults[0].name;
        addColumnRecCases(entity);
    }
    else {
        var wasEmpty = isObjectEmpty(manualQueue);
        var wasSingleton = getSecondValue(manualQueue) === undefined;
        manualQueue[entity["/rec_ui/id"]] = entity;
        if (wasEmpty)
            manualReconcile();
        else if (wasSingleton)
            renderReconChoices(entity)
    }
    autoReconcile();
}

/*
** Manual Reconciliation
*/

function manualReconcile() {
    if ($(".manualReconChoices:visible").length === 0) {
        var val = getFirstValue(manualQueue);
        if(val != undefined) {
            displayReconChoices(val["/rec_ui/id"])
            renderReconChoices(getSecondValue(manualQueue)); //render-ahead the next one
        }
        else{
            $(".manualQueueEmpty").show();
            $(".manualReconciliation").hide();
        }
    }
}

function displayReconChoices(entityID) {
    var entity = entities[entityID];
    if (entity === undefined) return;
    $(".manualQueueEmpty").hide();
    $(".manualReconciliation").show();
    if (! $("#manualReconcile" + entityID)[0])
        renderReconChoices(entity);
    $(".manualReconChoices:visible").remove();
    $("#manualReconcile" + entityID).show();
}

function renderReconChoices(entity) {
    if (entity == undefined) return;
    var template = $("#manualReconcileTemplate").clone();
    template[0].id = "manualReconcile" + entity['/rec_ui/id'];
    var headers = entity["/rec_ui/headers"];
    var mqlProps = entity["/rec_ui/mql_props"];

    var currentRecord = $(".recordVals",template);
    for(var i = 0; i < headers.length; i++) {
        currentRecord.append(node("label", getPropName(headers[i]) + ":", {"for":idToClass(headers[i])}));
        currentRecord.append(node("div", {"class":"propertyGroup"}).append(displayValue(entity.getChainedProperty(headers[i]))));
    }

    var tableHeader = $(".reconciliationCandidates table thead", template).empty();
    var columnHeaders = ["","Image","Name","Type"].concat($.map(mqlProps,getPropName)).concat(["Score"]);
    for (var i = 0; i < columnHeaders.length; i++)
        tableHeader.append(node("th",columnHeaders[i],{"class":"bottomHeader"}));

    var tableBody = $(".reconciliationCandidates table tbody", template).empty();
    for (var i = 0; i < entity.reconResults.length; i++){
        tableBody.append(renderCandidate(entity.reconResults[i], mqlProps, entity));
        fetchMqlProps(entity.reconResults[i], entity);
    }

    function updateCandidates() {
        $('.reconciliationCandidates table tbody tr:odd', template).addClass('odd');
        $('.reconciliationCandidates table tbody tr:even', template).addClass('even');
        numCandidates = entity.reconResults.length;
    }
    updateCandidates();
    $(".find_topic", template)
        .suggest({type:entity['/type/object/type'],
                  type_strict:"should",
                  flyout:true})
        .bind("fb-select", function(e, data) {
          entity['/rec_ui/freebase_name'] = $.makeArray(data.name);
          handleReconChoice(entity, data.id);
        });
    $(".otherSelection", template).click(function() {handleReconChoice(entity, this.name)});

    $(".moreButton",template).click(function() {
        $(".loadingMoreCandidates", template).fadeIn();
        getCandidates(entity, function() {
            $(".loadingMoreCandidates", template).hide();
            if (entity.reconResults.length <= numCandidates)
                return $(".moreButton",template).fadeOut();
            for (var i = numCandidates; i < entity.reconResults.length; i++){
                var candidate = renderCandidate(entity.reconResults[i], mqlProps, entity).appendTo(tableBody);
                fetchMqlProps(entity.reconResults[i], entity);
            }
            updateCandidates();

        });
    });
    template.insertAfter("#manualReconcileTemplate")
}

function renderCandidate(result, mqlProps, entity) {
    var url = freebase_url + "/view/" + result['id'];
    var tableRow = node("tr", {"class":idToClass(result["id"])});

    var button = node("button", "Select",
       {"class":'manualSelection',
        "name":result.id})
    tableRow.append(node("td",button));
    button.click(function(val) {entity['/rec_ui/freebase_name'] = result.name; handleReconChoice(entity, result.id)})

    node("td",
         node("img",{src:freebase_url + "/api/trans/image_thumb/"+result['id']+"?maxwidth=100&maxheight=100"})
    ).appendTo(tableRow);

    tableRow.append(node("td",displayValue(result)));
    var displayTypes = [];
    $.each($.makeArray(result.type), function(_,typeId) {
        //types that end in /topic are uninteresting
        if (typeId.match(/\/topic$/)) return;
        displayTypes.push(freebase.makeLink(typeId));
    })
    node("td").append(wrapForOverflow(displayTypes)).appendTo(tableRow);

    mqlProps = groupProperties(mqlProps).getPropsForRows();
    for(var j = 0; j < mqlProps.length; j++)
        tableRow.append(
            node("td", node("img",{src:"spinner.gif"}),
                 {"class":"replaceme "+idToClass(mqlProps[j])})
        );
    tableRow.append(node("td",result["score"]));
    return tableRow;
}

function fetchMqlProps(reconResult, entity) {
    var mqlProps = entity["/rec_ui/mql_props"];
    var query = {"id":reconResult["id"]};
    $.each(mqlProps, function(_, prop) {
        var slot = query;
        var parts = prop.split(":");
        $.each(parts.slice(0,parts.length-1), function(_, part) {
            slot[part] = slot[part] || [{optional:true}];
            slot = slot[part][0];
        });
        var lastPart = parts[parts.length-1];
        if (isValueProperty(lastPart))
            slot[lastPart] = [];
        else
            slot[lastPart] = [{"name":null,"id":null,"optional":true}];
    })
    var envelope = {query:query};
    function handler(results) {
        fillInMQLProps(entity, results);
        //don't show annoying loading symbols indefinitely if there's an error
        $("#manualReconcile" + entity["/rec_ui/id"] + " .replaceme").empty();
    }
    freebase.mqlRead(envelope,handler);
}

function fillInMQLProps(entity, mqlResult) {
    var context = $("#manualReconcile" + entity["/rec_ui/id"]);
    if (!mqlResult || mqlResult["code"] != "/api/status/ok" || mqlResult["result"] == null) {
        error(mqlResult);
        return;
    }

    var result = mqlResult.result;
    var entity = $("tr." + idToClass(result.id),context);

    for (var i = 0; i < mqlProps.length; i++) {
        var cell = $("td." + idToClass(mqlProps[i]), entity).empty();
        cell.append(displayValue(getChainedProperty(result, mqlProps[i])));
        cell.removeClass("replaceme");
    }
}

function handleReconChoice(entity,freebaseId) {
    delete manualQueue[entity["/rec_ui/id"]];
    $("#manualReconcile" + entity['/rec_ui/id']).remove();
    if (freebaseId != undefined){
        entity.id = freebaseId;
        canonicalizeFreebaseId(entity);
    }
    addColumnRecCases(entity);
    updateUnreconciledCount();
    manualReconcile();
}

function canonicalizeFreebaseId(entity) {
    var envelope = {query:{"myId:id":entity.id, "id":null}}
    freebase.mqlRead(envelope, function(results){
        if (results && results.result && results.result.id)
            entity.id = results.result.id
    });
}<|endoftext|>


<commit_before>/**
 * Rectangle: represents the location (x,y) and size (width,height) of a container
 */
function Rectangle(x,y,width,height){
	this.x = x;
	this.y = y;
	this.width = width;
	this.height = height;
}

/**
 * Container: represents a node in the tree structure of the windows
 *
 * \param dimensions A rectangle containing the container's size and location
 * \param window The window id of a window (when the container is a leaf, otherwise null)
 * \param parent The parent node of the container
 * \param mode The tiling mode of the container, either horizontal or vertical
 */
function Container(dimensions,windowID,parent,mode){
	this.dimensions = dimensions;
	this.window = windowID;
	this.parent = parent;
	this.tiling_mode = mode;
	this.children = [];

	/**
	 * Adds a child node to the list of children
	 * \param container The child node to be added
	 */
	this.addChild = function (container){
		var perc = 100/(children.length+1);
		setChildrenPercentages(perc);
		children.push({container: container, percentage: perc});
		//TODO:redraw children?
	}

	/**
	 * Sets the percentages of all children to perc
	 * \param perc The percentage that all children need to get
	 */
	this.setChildrenPercentages = function (perc){
		for(var i=0; i<children.length; i++){
			children[i].percentage = perc;
		}
	};

	/**
	 * Sets the window attribute if this container is a leaf node, otherwise an error occurs
	 * \param windowID The window id of the window
	 */
	this.setWindow = function (windowID){
		if(children.length === 0)
			this.windowID = windowID;
			//TODO:draw window?
		else
			console.error("Cannot set the window attribute of a non-leaf node");
	}

	/**
	 * Switches the tiling mode from vertical to horizontal or vice versa
	 */
	this.switchTilingMode = function (){
		if(this.tiling_mode === "horizontal")
			this.tiling_mode = "vertical";
		else
			this.tiling_mode = "horizontal";
		//TODO:redraw children
	}
}<commit_msg>Added margin as attibute of a container<commit_after>/**
 * Rectangle: represents the location (x,y) and size (width,height) of a container
 */
function Rectangle(x,y,width,height){
	this.x = x;
	this.y = y;
	this.width = width;
	this.height = height;
}

/**
 * Container: represents a node in the tree structure of the windows
 *
 * \param dimensions A rectangle containing the container's size and location
 * \param window The window id of a window (when the container is a leaf, otherwise null)
 * \param parent The parent node of the container
 * \param mode The tiling mode of the container, either horizontal or vertical
 */
function Container(dimensions,windowID,parent,mode){
	this.dimensions = dimensions;
	this.window = windowID;
	this.parent = parent;
	this.tiling_mode = mode;
	this.children = [];
	this.margin = 10;// The number of pixels between each window and the border

	/**
	 * Adds a child node to the list of children
	 * \param container The child node to be added
	 */
	this.addChild = function (container){
		var perc = 100/(children.length+1);
		setChildrenPercentages(perc);
		children.push({container: container, percentage: perc});
		//TODO:redraw children?
	}

	/**
	 * Sets the percentages of all children to perc
	 * \param perc The percentage that all children need to get
	 */
	this.setChildrenPercentages = function (perc){
		for(var i=0; i<children.length; i++){
			children[i].percentage = perc;
		}
	};

	/**
	 * Sets the window attribute if this container is a leaf node, otherwise an error occurs
	 * \param windowID The window id of the window
	 */
	this.setWindow = function (windowID){
		if(children.length === 0)
			this.windowID = windowID;
			//TODO:draw window?
		else
			console.error("Cannot set the window attribute of a non-leaf node");
	}

	/**
	 * Switches the tiling mode from vertical to horizontal or vice versa
	 */
	this.switchTilingMode = function (){
		if(this.tiling_mode === "horizontal")
			this.tiling_mode = "vertical";
		else
			this.tiling_mode = "horizontal";
		//TODO:redraw children
	}
}<|endoftext|>


<commit_before>const Boom = require('boom');
const {trimSlash, normalizePath, is_valid_method} = require('./helpers');
const _ = require('lodash');
const Mongoose = require('mongoose');

module.exports = class Controller {

    /**
     *
     * @param config
     */
    constructor(config) {
        // Keep config
        this.config = config || {};

        // Function binds
        this._internal_error = this._internal_error.bind(this);

        // Routes prefix
        this.config.prefix = trimSlash(this.config.prefix || '');

        // Default routes method
        if (!this.config.default_method) this.config.default_method = 'get';

        // Default config that applies all routes
        if (!this.config.default) this.config.default = {};

        // Routes specific config (overrides default_config)
        if (!this.config.routes) this.config.routes = {};

        // Models that can be injected to routes!
        if (!this.config.models) this.config.models = {};

    }

    // ==================================================
    // Routing
    // ==================================================

    /**
     * Returns prefixed controller routes compatible with HAPI api
     * @returns {Array}
     */
    routes() {
        let routes = [];

        Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach((prop) => {
            // Skip internal functions
            if (prop === 'constructor' || (prop.length > 1 && prop.indexOf('_') === 0)) return;

            // Make route object
            let route = {};

            // Route config
            route.config = _.extend({
                id: this.constructor.name + '.' + prop
            }, this.config.default, this.config.routes[prop]);

            // Generate path and method based on prop name
            let split = prop.split('_');

            // Route method
            route.method = this.config.default_method;
            if (is_valid_method(split[split.length - 1]))
                route.method = split.pop();

            // Prepend prefix to split
            if (this.config.prefix && this.config.prefix.length) {
                split = this.config.prefix.split('/').concat(split);
            }

            // Convert route parameters from $param to {param} syntax
            let route_params = [];
            split = split.map((s) => {
                if (s.indexOf('$') === 0) {
                    let is_optional = (s.indexOf('$$') === 0);
                    let param = s.substr(is_optional ? 2 : 1);
                    s = '{' + param + (is_optional ? '?' : '') + '}';
                    route_params.push({param_id: param, is_optional});
                }
                return s;
            });

            // Route path
            route.path = normalizePath('/' + split.join('/'));

            // Route handler
            let _handler = this[prop];

            // Create safe handler that wraps function and injects route params
            route.handler = (request, reply) => {
                this._resolve_handler_params(request, reply, route_params).then((resolved_params) => {
                    if (!resolved_params || reply._replied) return;

                    return _handler.call(this, request, reply, request.auth.credentials, resolved_params);

                }).catch((err) => this._internal_error(err, route, request, reply));
            };

            // Push route
            routes.push(route);
        });

        return routes;
    }

    _internal_error(error, route, request, reply) {

        // Prepare reply with 500 error to client
        let reply_error_called = false;
        const reply_error = (ref) => {
            if (reply && !reply_error_called && !reply._replied)
                reply({error: 'Internal server error', statusCode: 500, ref}).code(500);
            reply_error_called = true;
        };

        // Report error to hapi instance
        const tags = [
            'error',
            'controller:' + this.constructor.name,
            'route_id:' + route.config.id,
        ];

        const context = {
            error,
            payload: request.payload,
            query: request.query,
            user: request.auth.credentials,
        };

        //
        if (this.hapi.sentry) {
            context._sentry_callback = reply_error;
            setTimeout(reply_error, 4000); // Fallback if sentry timed out!
        } else {
            reply_error();
        }

        //
        this.hapi.log(tags, context);

    }

    _resolve_handler_params(request, reply, route_params) {
        let queue = [];
        let resolved_params = {};

        route_params.forEach(({param_id, is_optional}) => {
            let request_param = request.params[param_id];

            // Skip optional params if are not provided
            if (is_optional && !request_param) {
                resolved_params[param_id] = null;
                return;
            }

            // Find param's model
            let model = this.config.models[_.capitalize(param_id)];

            // Directly resolve params without model
            if (!model) {
                resolved_params[param_id] = request_param;
                return;
            }

            // Prepare query
            let routeKey = model.$routeKey || '_id';
            if (routeKey instanceof Function) routeKey = routeKey();
            let query = {};
            query[routeKey] = request_param.toString();

            // If query is on _id ensure value is valid objectId, else reply error
            if (routeKey == '_id' && !Mongoose.Types.ObjectId.isValid(request_param)) {
                if (!reply._replied)
                    reply(Boom.badRequest("invalid id for " + param_id));
                return null;
            }

            // Push find job to queue
            queue.push(model.findOne(query).then((item) => {
                if (!item) {
                    throw {error: "no records found for " + param_id};
                }
                resolved_params[param_id] = item;
            }));
        });

        return Promise.all(queue).then(() => {
            return resolved_params;
        }).catch((err) => {
            if (!reply._replied)
                reply(Boom.notFound(err.error));
            return null;
        });
    }

};

<commit_msg>Handle Boom exceptions without logging<commit_after>const Boom = require('boom');
const {trimSlash, normalizePath, is_valid_method} = require('./helpers');
const _ = require('lodash');
const Mongoose = require('mongoose');

module.exports = class Controller {

    /**
     *
     * @param config
     */
    constructor(config) {
        // Keep config
        this.config = config || {};

        // Function binds
        this._internal_error = this._internal_error.bind(this);

        // Routes prefix
        this.config.prefix = trimSlash(this.config.prefix || '');

        // Default routes method
        if (!this.config.default_method) this.config.default_method = 'get';

        // Default config that applies all routes
        if (!this.config.default) this.config.default = {};

        // Routes specific config (overrides default_config)
        if (!this.config.routes) this.config.routes = {};

        // Models that can be injected to routes!
        if (!this.config.models) this.config.models = {};

    }

    // ==================================================
    // Routing
    // ==================================================

    /**
     * Returns prefixed controller routes compatible with HAPI api
     * @returns {Array}
     */
    routes() {
        let routes = [];

        Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach((prop) => {
            // Skip internal functions
            if (prop === 'constructor' || (prop.length > 1 && prop.indexOf('_') === 0)) return;

            // Make route object
            let route = {};

            // Route config
            route.config = _.extend({
                id: this.constructor.name + '.' + prop
            }, this.config.default, this.config.routes[prop]);

            // Generate path and method based on prop name
            let split = prop.split('_');

            // Route method
            route.method = this.config.default_method;
            if (is_valid_method(split[split.length - 1]))
                route.method = split.pop();

            // Prepend prefix to split
            if (this.config.prefix && this.config.prefix.length) {
                split = this.config.prefix.split('/').concat(split);
            }

            // Convert route parameters from $param to {param} syntax
            let route_params = [];
            split = split.map((s) => {
                if (s.indexOf('$') === 0) {
                    let is_optional = (s.indexOf('$$') === 0);
                    let param = s.substr(is_optional ? 2 : 1);
                    s = '{' + param + (is_optional ? '?' : '') + '}';
                    route_params.push({param_id: param, is_optional});
                }
                return s;
            });

            // Route path
            route.path = normalizePath('/' + split.join('/'));

            // Route handler
            let _handler = this[prop];

            // Create safe handler that wraps function and injects route params
            route.handler = (request, reply) => {
                this._resolve_handler_params(request, reply, route_params).then((resolved_params) => {
                    if (!resolved_params || reply._replied) return;

                    return _handler.call(this, request, reply, request.auth.credentials, resolved_params);

                }).catch((err) => this._internal_error(err, route, request, reply));
            };

            // Push route
            routes.push(route);
        });

        return routes;
    }

    _internal_error(error, route, request, reply) {

        // Handle Boom exceptions without logging
        if (error && error.isBoom) {
            if (reply && !reply._replied)
                reply(error);
            return;
        }

        // Prepare reply with 500 error to client
        let reply_error_called = false;
        const reply_error = (ref) => {
            if (reply && !reply_error_called && !reply._replied)
                reply({error: 'Internal server error', statusCode: 500, ref}).code(500);
            reply_error_called = true;
        };

        // Report error to hapi instance
        const tags = [
            'error',
            'controller:' + this.constructor.name,
            'route_id:' + route.config.id,
        ];

        const context = {
            error,
            payload: request.payload,
            query: request.query,
            user: request.auth.credentials,
        };

        //
        if (this.hapi.sentry) {
            context._sentry_callback = reply_error;
            setTimeout(reply_error, 4000); // Fallback if sentry timed out!
        } else {
            reply_error();
        }

        //
        this.hapi.log(tags, context);

    }

    _resolve_handler_params(request, reply, route_params) {
        let queue = [];
        let resolved_params = {};

        route_params.forEach(({param_id, is_optional}) => {
            let request_param = request.params[param_id];

            // Skip optional params if are not provided
            if (is_optional && !request_param) {
                resolved_params[param_id] = null;
                return;
            }

            // Find param's model
            let model = this.config.models[_.capitalize(param_id)];

            // Directly resolve params without model
            if (!model) {
                resolved_params[param_id] = request_param;
                return;
            }

            // Prepare query
            let routeKey = model.$routeKey || '_id';
            if (routeKey instanceof Function) routeKey = routeKey();
            let query = {};
            query[routeKey] = request_param.toString();

            // If query is on _id ensure value is valid objectId, else reply error
            if (routeKey == '_id' && !Mongoose.Types.ObjectId.isValid(request_param)) {
                if (!reply._replied)
                    reply(Boom.badRequest("invalid id for " + param_id));
                return null;
            }

            // Push find job to queue
            queue.push(model.findOne(query).then((item) => {
                if (!item) {
                    throw {error: "no records found for " + param_id};
                }
                resolved_params[param_id] = item;
            }));
        });

        return Promise.all(queue).then(() => {
            return resolved_params;
        }).catch((err) => {
            if (!reply._replied)
                reply(Boom.notFound(err.error));
            return null;
        });
    }

};

<|endoftext|>