<commit_before>import Repo from './repo';
import { ObjectPathAccessor } from 'mzen-schema';

export class RepoPopulate
{
  relationRepo: Repo;
  
  constructor(relationRepo: Repo)
  {
    this.relationRepo = relationRepo;
  }
  
  hasMany(docs, options)
  {
    options.relation = 'hasMany';
    return this.has(docs, options);
  }
  
  hasOne(docs, options)
  {
    options.relation = 'hasOne';
    return this.has(docs, options);
  }
  
  async has(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);

    // Use query option if provided - allows results to be further filtered in addition to relation id
    options.query[options.key] = {$in: relationIds};
    // @ts-ignore - Expected 0 arguments, but got 2 - variable method arguments
    var relatedDocs = await this.relationRepo.find(options.query, options);
    // Group related docs by parent key
    var values = {};
    relatedDocs.forEach(function(relatedDoc){
      if (values[relatedDoc[options.key]] == undefined) values[relatedDoc[options.key]] = [];
      values[relatedDoc[options.key]].push(relatedDoc);
    });

    return this.populate(docs, options, values);
  }
  
  async hasManyCount(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);

    // Use query option if provided - allows results to be further filtered in addition to relation id
    options.query[options.key] = {$in: relationIds};

    var aggregateId = {};
    aggregateId[options.key] = '$' + options.key;
    var results = await this.relationRepo.aggregate([
      {$match: options.query},
      {
        $group : {
           _id : aggregateId,
           count: { $sum: 1 }
        }
      }
    ]);

    // Group related docs by parent key
    var values = {};
    results.forEach(function(result){
      if (values[result['_id'][options.key]] == undefined) values[result['_id'][options.key]] = 0;
      values[result['_id'][options.key]] = result['count'];
    });

    return this.populate(docs, options, values);
  }
  
  embeddedHasOne(docs, options)
  {
    options.relation = 'embeddedHasOne';
    return this.embeddedHas(docs, options);
  }
  
  embeddedHasMany(docs, options)
  {
    options.relation = 'embeddedHasMany';
    return this.embeddedHas(docs, options);
  }
  
  embeddedHas(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);
    const embeddedDocs = this.getEmebedRelations(options.docPathRelated, docs);

    var values = {};
    embeddedDocs.forEach(function(embeddedDoc){
      if (relationIds.indexOf(embeddedDoc[options.key]) != -1) {
        if (values[embeddedDoc[options.key]] == undefined) values[embeddedDoc[options.key]] = [];
        values[embeddedDoc[options.key]].push(embeddedDoc);
      }
    });

    // Use query option if provided - allows results to be further filtered in addition to relation id
    return Promise.resolve(this.populate(docs, options, values));
  }
  
  belongsToOne(docs, options)
  {
    options.relation = 'belongsToOne';
    return this.belongsTo(docs, options);
  }
  
  belongsToMany(docs, options)
  {
    options.relation = 'belongsToMany';
    return this.belongsTo(docs, options);
  }
  
  async belongsTo(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);
    // Use query option if provided - allows results to be further filtered in addition to relation id
    options.query[options.pkey] = {$in: relationIds};
    // @ts-ignore - Expected 0 arguments, but got 2 - variable method arguments
    var relatedDocs = await this.relationRepo.find(options.query, options);
    // Group related docs by parent key
    let values = {};
    
    relatedDocs.forEach(function(relatedDoc, x){
      values[relatedDoc[options.pkey]] = relatedDocs[x];
    });

    return this.populate(docs, options, values);
  }
  
  embeddedBelongsToMany(docs, options)
  {
    options.relation = 'embeddedBelongsToMany';
    return this.embeddedBelongsTo(docs, options);
  }
  
  embeddedBelongsToOne(docs, options)
  {
    options.relation = 'embeddedBelongsToOne';
    return this.embeddedBelongsTo(docs, options);
  }
  
  embeddedBelongsTo(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);
    const embeddedDocs = this.getEmebedRelations(options.docPathRelated, docs);

    var values = {};
    embeddedDocs.forEach((embeddedDoc) => {
      // We must store the id as a primitive so we can use it as a object field name in our lookup values object
      // - complex types can not be used as object field names
      let id = String(embeddedDoc[options.pkey]);
      if (relationIds.indexOf(id) != -1) {
        // Group related docs by parent key
        values[id] = embeddedDoc;
      }
    });

    // Use query option if provided - allows results to be further filtered in addition to relation id
    return Promise.resolve(this.populate(docs, options, values));
  }
  
  getRelationIds(docs, options)
  {
    this.normalizeOptions(options);

    // We may be passed an array of docs or a single doc
    // - we want to deal with both situations uniformly
    // - if we are passed a single object we make it an array
    docs = Array.isArray(docs) ? docs : [docs];
    var relationIds = [];
    // If we were given a document path, the path is relative to the object not to the array of objects
    // - prefix the document path with '*' so it will pull all elements from the array
    const docPath = options.docPath ? '*.' + options.docPath : '*';
    const embeddedDocs = ObjectPathAccessor.getPath(docPath, docs);

    embeddedDocs.forEach(function(doc){
      if (doc) {
        let id = doc[options.sourceKey];
        if (Array.isArray(id)) {
          // Only belongsToMany relation supports an array of source keys
          id.forEach((anId) => {
            // We must store the id as a primitive as they are referenced as by lookup object
            // - complex types can not be used as object field names
            anId = String(anId);
            if (relationIds.indexOf(anId) == -1) {
              relationIds.push(anId);
            }
          });
        } else {
          // We must store the id as a primitive as they are referenced as by lookup object
          // - complex types can not be used as object field names
          relationIds.push(String(id));
        }
      }
    });

    return relationIds;
  }
  
  getEmebedRelations(path, docs)
  {
    // We may be passed an array of docs or a single doc
    // - we want to deal with both situations uniformly
    // - if we are passed a single object we make it an array
    docs = Array.isArray(docs) ? docs : [docs];
    // If we were given a document path, the path is relative to the object not to the array of objects
    // - prefix the document path with '*' so it will pull all elements from the array
    const docPath = path ? '*.' + path : '*';
    const embeddedDocs = ObjectPathAccessor.getPath(docPath, docs);
    return embeddedDocs;
  }
  
  populate(docs, options, values)
  {
    this.normalizeOptions(options);

    // We may be passed an array of docs or a single doc
    // - we want to deal with both situations uniformly
    // - if we are passed a single object we make it an array
    docs = Array.isArray(docs) ? docs : [docs];
    // If we were given a document path, the path is relative to the object not to the array of objects
    // - prefix the document path with '*' so it will pull all elements from the array
    const docPath = options.docPath ? '*.' + options.docPath : '*';
    const embeddedDocs = ObjectPathAccessor.getPath(docPath, docs);

    embeddedDocs.forEach(function(doc){
      if (doc !== Object(doc)) return;
      var sourceKey = doc[options.sourceKey];
      if (sourceKey) {
        if (Array.isArray(sourceKey)) {
          // The source key is an array of keys so we need to look up each value and append
          // - this must be a belongsToMany relation since that is the only relation type that supports
          // - an array of source keys
          doc[options.alias] = [];
          sourceKey.forEach(function(sk){
            if (values[sk] !== undefined) doc[options.alias].push(values[sk]);
          });
        } else {
          let isHasOne = (options.relation.toLowerCase().indexOf('hasone') != -1);
          if (values[sourceKey] !== undefined) doc[options.alias] = isHasOne ? values[sourceKey][0] : values[sourceKey];
        }
      }
    });

    return docs;
  }
  
  normalizeOptions(options)
  {
    // pkey is the primary key name to use when looking up relations
    // - the primary key is the key of the source document on has* type relations
    // - the primary key is the key of the related document on blongsTo* type relations
    options.pkey = options['pkey'] ? options['pkey'] : '_id';
    // Key is the field where the relation id is stored in the related document
    // - for all except belongsTo type relations where the key is on the source document
    options.key = options['key'] ? options['key'] : '';

    // relation type name
    options.relation = options['relation'] ? options['relation'] : '';

    // sourceKey is an internal option which refers to either the key or the pkey depending on relation type
    // - sourceKey is the same as key value on belongsTo* type relations
    // - sourceKey is the same as pkey value on has* type relations
    let isBelongsTo = (options['relation'].toLowerCase().indexOf('belongsto') != -1);
    options.sourceKey = isBelongsTo ? options['key'] : options['pkey'];

    // alias is the field name used to store the compiled relations
    options.alias = options['alias'] ? options['alias'] : '';
    // docPath is path to the object(s) where the relation should be populated
    options.docPath = options['docPath'] ? options['docPath'] : false;
    // docPathRelated is the path to the related objects
    options.docPathRelated = options['docPathRelated'] ? options['docPathRelated'] : false;

    // query is query object used to further filter related objects
    options.query = options['query'] ? options['query'] : {};

    options.populateRelations = options['populateRelations'] !== false;
  }
}

export default RepoPopulate;<commit_msg>Refactor repo populate<commit_after>import Repo from './repo';
import { ObjectPathAccessor } from 'mzen-schema';

export class RepoPopulate
{
  relationRepo: Repo;
  
  constructor(relationRepo: Repo)
  {
    this.relationRepo = relationRepo;
  }
  
  hasMany(docs, options)
  {
    options.relation = 'hasMany';
    return this.has(docs, options);
  }
  
  hasOne(docs, options)
  {
    options.relation = 'hasOne';
    return this.has(docs, options);
  }
  
  async has(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);

    // Use query option if provided - allows results to be further filtered in addition to relation id
    options.query[options.key] = {$in: relationIds};
    // @ts-ignore - Expected 0 arguments, but got 2 - variable method arguments
    var relatedDocs = await this.relationRepo.find(options.query, options);
    // Group related docs by parent key
    var values = {};
    relatedDocs.forEach(function(relatedDoc){
      if (values[relatedDoc[options.key]] == undefined) values[relatedDoc[options.key]] = [];
      values[relatedDoc[options.key]].push(relatedDoc);
    });

    return this.populate(docs, options, values);
  }
  
  async hasManyCount(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);

    // Use query option if provided - allows results to be further filtered in addition to relation id
    options.query[options.key] = {$in: relationIds};

    var aggregateId = {};
    aggregateId[options.key] = '$' + options.key;
    var results = await this.relationRepo.aggregate([
      {$match: options.query},
      {
        $group : {
           _id : aggregateId,
           count: { $sum: 1 }
        }
      }
    ]);

    // Group related docs by parent key
    var values = {};
    results.forEach(function(result){
      if (values[result['_id'][options.key]] == undefined) values[result['_id'][options.key]] = 0;
      values[result['_id'][options.key]] = result['count'];
    });

    return this.populate(docs, options, values);
  }
  
  embeddedHasOne(docs, options)
  {
    options.relation = 'embeddedHasOne';
    return this.embeddedHas(docs, options);
  }
  
  embeddedHasMany(docs, options)
  {
    options.relation = 'embeddedHasMany';
    return this.embeddedHas(docs, options);
  }
  
  embeddedHas(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);
    const embeddedDocs = this.getEmebedRelations(options.docPathRelated, docs);

    var values = {};
    embeddedDocs.forEach(function(embeddedDoc){
      if (relationIds.indexOf(embeddedDoc[options.key]) != -1) {
        if (values[embeddedDoc[options.key]] == undefined) values[embeddedDoc[options.key]] = [];
        values[embeddedDoc[options.key]].push(embeddedDoc);
      }
    });

    // Use query option if provided - allows results to be further filtered in addition to relation id
    return Promise.resolve(this.populate(docs, options, values));
  }
  
  belongsToOne(docs, options)
  {
    options.relation = 'belongsToOne';
    return this.belongsTo(docs, options);
  }
  
  belongsToMany(docs, options)
  {
    options.relation = 'belongsToMany';
    return this.belongsTo(docs, options);
  }
  
  async belongsTo(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);
    // Use query option if provided - allows results to be further filtered in addition to relation id
    options.query[options.pkey] = {$in: relationIds};
    // @ts-ignore - Expected 0 arguments, but got 2 - variable method arguments
    var relatedDocs = await this.relationRepo.find(options.query, options);
    // Group related docs by parent key
    let values = {};
    
    relatedDocs.forEach(function(relatedDoc, x){
      values[relatedDoc[options.pkey]] = relatedDocs[x];
    });

    return this.populate(docs, options, values);
  }
  
  embeddedBelongsToMany(docs, options)
  {
    options.relation = 'embeddedBelongsToMany';
    return this.embeddedBelongsTo(docs, options);
  }
  
  embeddedBelongsToOne(docs, options)
  {
    options.relation = 'embeddedBelongsToOne';
    return this.embeddedBelongsTo(docs, options);
  }
  
  embeddedBelongsTo(docs, options)
  {
    this.normalizeOptions(options);

    const relationIds = RepoPopulate.prototype.getRelationIds(docs, options);
    const embeddedDocs = this.getEmebedRelations(options.docPathRelated, docs);

    var values = {};
    embeddedDocs.forEach((embeddedDoc) => {
      // We must store the id as a primitive so we can use it as a object field name in our lookup values object
      // - complex types can not be used as object field names
      let id = String(embeddedDoc[options.pkey]);
      if (relationIds.indexOf(id) != -1) {
        // Group related docs by parent key
        values[id] = embeddedDoc;
      }
    });

    // Use query option if provided - allows results to be further filtered in addition to relation id
    return Promise.resolve(this.populate(docs, options, values));
  }
  
  getRelationIds(docs, options)
  {
    this.normalizeOptions(options);

    // We may be passed an array of docs or a single doc
    // - we want to deal with both situations uniformly
    // - if we are passed a single object we make it an array
    docs = Array.isArray(docs) ? docs : [docs];
    var relationIds = [];
    // If we were given a document path, the path is relative to the object not to the array of objects
    // - prefix the document path with '*' so it will pull all elements from the array
    const docPath = options.docPath ? '*.' + options.docPath : '*';
    const embeddedDocs = ObjectPathAccessor.getPath(docPath, docs);

    embeddedDocs.forEach(function(doc){
      if (doc) {
        let id = doc[options.sourceKey];
        if (Array.isArray(id)) {
          // Only belongsToMany relation supports an array of source keys
          id.forEach((anId) => {
            // We must store the id as a primitive as they are referenced as by lookup object
            // - complex types can not be used as object field names
            anId = String(anId);
            if (relationIds.indexOf(anId) == -1) {
              relationIds.push(anId);
            }
          });
        } else {
          // We must store the id as a primitive as they are referenced as by lookup object
          // - complex types can not be used as object field names
          relationIds.push(String(id));
        }
      }
    });

    return relationIds;
  }
  
  getEmebedRelations(path, docs)
  {
    // We may be passed an array of docs or a single doc
    // - we want to deal with both situations uniformly
    // - if we are passed a single object we make it an array
    docs = Array.isArray(docs) ? docs : [docs];
    // If we were given a document path, the path is relative to the object not to the array of objects
    // - prefix the document path with '*' so it will pull all elements from the array
    const docPath = path ? '*.' + path : '*';
    const embeddedDocs = ObjectPathAccessor.getPath(docPath, docs);
    return embeddedDocs;
  }
  
  populate(docs, options, values)
  {
    this.normalizeOptions(options);

    // We may be passed an array of docs or a single doc
    // - we want to deal with both situations uniformly
    // - if we are passed a single object we make it an array
    docs = Array.isArray(docs) ? docs : [docs];
    // If we were given a document path, the path is relative to the object not to the array of objects
    // - prefix the document path with '*' so it will pull all elements from the array
    const docPath = options.docPath ? '*.' + options.docPath : '*';
    const embeddedDocs = ObjectPathAccessor.getPath(docPath, docs);

    embeddedDocs.forEach(function(doc){
      if (doc !== Object(doc)) return;
      var sourceKey = doc[options.sourceKey];
      if (sourceKey) {
        if (Array.isArray(sourceKey)) {
          // The source key is an array of keys so we need to look up each value and append
          // - this must be a belongsToMany relation since that is the only relation type that supports
          // - an array of source keys
          doc[options.alias] = [];
          sourceKey.forEach(function(sk){
            if (values[sk] !== undefined) doc[options.alias].push(values[sk]);
          });
        } else {
          let isHasOne = (options.relation.toLowerCase().indexOf('hasone') != -1);
          if (values[sourceKey] !== undefined) doc[options.alias] = isHasOne ? values[sourceKey][0] : values[sourceKey];
        }
      }
    });

    return docs;
  }
  
  normalizeOptions(options)
  {
    // pkey is the primary key name to use when looking up relations
    // - the primary key is the key of the source document on has* type relations
    // - the primary key is the key of the related document on blongsTo* type relations
    options.pkey = options.pkey ? options.pkey : '_id';
    // Key is the field where the relation id is stored in the related document
    // - for all except belongsTo type relations where the key is on the source document
    options.key = options.key ? options.key : '';

    // relation type name
    options.relation = options.relation ? options.relation : '';

    // sourceKey is an internal option which refers to either the key or the pkey depending on relation type
    // - sourceKey is the same as key value on belongsTo* type relations
    // - sourceKey is the same as pkey value on has* type relations
    let isBelongsTo = (options.relation.toLowerCase().indexOf('belongsto') != -1);
    options.sourceKey = isBelongsTo ? options.key : options.pkey;

    // alias is the field name used to store the compiled relations
    options.alias = options.alias ? options.alias : '';
    // docPath is path to the object(s) where the relation should be populated
    options.docPath = options.docPath ? options.docPath : false;
    // docPathRelated is the path to the related objects
    options.docPathRelated = options.docPathRelated ? options.docPathRelated : false;

    // query is query object used to further filter related objects
    options.query = options.query ? options.query : {};

    options.populate = options.populate !== false;
  }
}

export default RepoPopulate;
{'fraction_non_alphanumeric': 0.1060931899641577, 'fraction_numerical': 0.0011469534050179211, 'mean_word_length': 2.8744676911683023, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5246605', 'n_tokens_mistral': 6003, 'n_tokens_neox': 5854, 'n_words': 1931}