<commit_before>// `@apostrophecms/pieces-widget-type` provides widgets that display display pieces of a
// particular type around the site.
//
// You will `extend` this module in new modules corresponding to your modules
// that extend `@apostrophecms/piece-type`.
//
// To learn more and see complete examples, see:
//
// [Reusable content with pieces](../../tutorials/getting-started/reusable-content-with-pieces.html)
//
// ## options
//
// ### `loadManyById`
//
// If `true` (the default), Apostrophe will take all the widgets passed to the `load` method for which pieces
// were chosen "by id", i.e. manually in a join, and do a single query to fetch them. This is usually more efficient,
// however if you have customized the `loadOne` method you may not wish to use it.

const _ = require('lodash');

module.exports = {
  extend: '@apostrophecms/widget-type',
  options: {
    loadManyById: true,
    builders: {}
  },
  beforeSuperClass(self, options) {
    self.piecesModuleName = options.piecesModuleName || self.__meta.name.replace(/-widgets$/, '');

    self.pieces = options.apos.modules[self.piecesModuleName];
    if (!self.pieces) {
      if (!options.piecesModuleName) {
        throw new Error('The module ' + self.__meta.name + ' extends @apostrophecms/pieces-widget-type, but the piecesModuleName option is not set, and we can\'t guess it from the name of your module.');
      } else {
        throw new Error('The module ' + self.__meta.name + ' has a piecesModuleName option that does not match any existing module.');
      }
    }
    options.label = options.label || self.pieces.pluralLabel;
    // A pieces widget generally doesn't edit things when you click "Edit,"
    // it selects things (although you can create and select things, or
    // edit and then select things)
    self.editLabel = options.editLabel || 'Select ' + options.label;
    let by;
    if (options.by) {
      by = options.by;
    } else {
      by = [
        'all',
        'id'
      ];
    }

    let addFields = [];

    let byChoicesInfo = {
      id: {
        value: 'id',
        label: 'Individually',
        showFields: ['_pieces']
      },
      all: {
        value: 'all',
        label: options.byAllLabel || 'All',
        showFields: ['limitByAll']
      }
    };

    let byChoices = _.map(_.filter(by, function (source) {
      return _.has(byChoicesInfo, source);
    }), function (source) {
      return byChoicesInfo[source];
    });

    addFields.push({
      type: 'select',
      name: 'by',
      // This just keeps the words "id", "all" and "tag" from cluttering
      // up the search index. It has NO impact on the searchability
      // of the pieces. -Tom
      searchable: false,
      label: 'Select...',
      def: byChoices[0] && byChoices[0].value,
      choices: byChoices,
      contextual: byChoices.length <= 1
    });

    if (_.includes(by, 'all')) {
      addFields.push({
        type: 'integer',
        name: 'limitByAll',
        label: 'Maximum displayed',
        def: 5
      });
    }

    if (_.includes(by, 'id')) {
      addFields.push({
        type: 'joinByArray',
        name: '_pieces',
        label: byChoices.length > 1 ? 'Individually' : 'Select...',
        idsField: 'pieceIds',
        withType: self.pieces.name
      });
    }

    options.addFields = addFields.concat(options.addFields || []);

    options.browser = _.defaults(options.browser || {}, { piecesModuleName: self.piecesModuleName });
  },
  init(self, options) {

    self.filters = options.filters;
  },
  methods(self, options) {
    return {

      // Load the appropriate pieces for each widget in the array. Apostrophe will try to feed
      // us as many at once as it can to cut down on database queries. We'll take all the
      // widgets for which pieces were chosen "by id" and do a single query, via
      // self.loadManyById. For everything we'll call self.loadOne individually, via
      // self.loadOthersOneAtATime. But in ALL cases, we invoke self.afterLoadOne for
      // each widget, allowing an opportunity to do custom work without thinking
      // about all this.
      //
      // Also in all cases, joins found in the schema other than the `_pieces` join
      // are loaded in the normal way for each widget.

      async load(req, widgets) {
        // Since we are overriding the default load method of `@apostrophecms/widget-type`, make
        // sure we still implement the `scene` option for assets

        if (self.options.scene) {
          req.scene = self.options.scene;
        }

        if (!self.options.loadManyById) {
          // Carrying out one big query for all widgets that select pieces by id has been
          // disabled for this module
          await self.loadOthersOneAtATime(req, widgets);
          return self.loadOtherJoins(req, widgets);
        }
        const byId = _.filter(widgets, { by: 'id' });
        const byOther = _.difference(widgets, byId);
        await self.loadManyById(req, byId);
        await self.loadOthersOneAtATime(req, byOther);
        return self.loadOtherJoins(req, widgets);
      },

      // Load many widgets, all of which were set to choose pieces "by id." This allows
      // Apostrophe to work efficiently when a page contains many pieces widgets in an
      // array, etc. This method is called by self.load, you don't need to call it yourself.
      //
      // This method still calls afterLoadOne for each widget, so there is still a simple
      // way to go beyond this if you need to do something fancy after a widget has been
      // through the normal loading process.

      async loadManyById(req, widgets) {

        const ids = _.reduce(widgets, function (ids, widget) {
          return ids.concat(widget.pieceIds || []);
        }, []);

        const widgetsByPieceId = {};

        _.each(widgets, function (widget) {
          widget._pieces = [];
          widget._piecesById = {};
          _.each(widget.pieceIds || [], function (_id) {
            if (!_.has(widgetsByPieceId, _id)) {
              widgetsByPieceId[_id] = [];
            }
            widgetsByPieceId[_id].push(widget);
          });
        });

        const cursor = self.widgetCursor(req, { _id: { $in: ids } });

        const pieces = await cursor.toArray();

        _.each(pieces, function (piece) {
          if (_.has(widgetsByPieceId, piece._id)) {
            _.each(widgetsByPieceId[piece._id], function (widget) {
              if (!_.has(widget._piecesById, piece._id)) {
                self.pushPieceForWidget(widget, piece);
                widget._piecesById[piece._id] = true;
              }
            });
          }
        });

        // Make sure pieces are returned in the order they appear in the list chosen by the user
        _.each(widgets, function (widget) {
          self.orderPiecesForWidget(widget);
        });

        // Give every widget a chance to have special sauce in its afterLoadOne method
        for (const widget of widgets) {
          await self.afterLoadOne(req, widget);
        }
      },

      // Load widgets that were NOT set to choose pieces "by id." Feeds them all
      // through self.loadOne and self.afterLoadOne. You don't have to call this,
      // self.load calls it for you.

      async loadOthersOneAtATime(req, widgets) {
        for (const widget of widgets) {
          await self.loadOne(req, widget);
          await self.afterLoadOne(req, widget);
        }
      },

      // Load related content for a single widget. This method is invoked only
      // if the `loadManyById` option has been set to `false` for your subclass.
      // Doing so has performance costs when widgets are numerous.

      async loadOne(req, widget) {
        // Go get stuff by tag / by all, with limit, attach to widget, then...
        const cursor = self.widgetCursor(req, {});
        if (widget.by === 'all') {
          // We are interested in everything
          cursor.limit(widget.limitByAll || 5);
        } else if (widget.by === 'id') {
          // By default, "by id" goes through a separate path, but support it here
          // so that the loadOne method can be used directly and naively to load
          // a widget, no matter how it is set up. Also useful if the loadManyById
          // option is disabled
          cursor.and({ _id: { $in: widget.pieceIds || [] } });
        }
        const pieces = await cursor.toArray();
        self.attachPiecesToWidget(widget, pieces);
      },

      // Load additional joins in the schema, if any, beyond the `_pieces`
      // join that we have implemented ourselves with higher performance

      async loadOtherJoins(req, widgets) {
        const schema = _.filter(self.schema, function (field) {
          return field.type.match(/^join/) && field.name !== '_pieces';
        });
        return self.apos.schemas.join(req, schema, widgets, undefined);
      },

      // Given an array of pieces, this method attaches them to the widget
      // as the _pieces property correctly with pushPiecesToWidget, and
      // orders them correctly if the user chose them in a specific order

      attachPiecesToWidget(widget, pieces) {
        widget._pieces = [];
        _.each(pieces, function (piece) {
          self.pushPieceForWidget(widget, piece);
        });
        self.orderPiecesForWidget(widget);
      },

      // A utility method that puts the pieces loaded for the widget in the
      // order requested by the user. widget._pieces should already be loaded
      // at this point. Called for you by the widget loader methods; useful
      // if you are overriding loadOne and disabling loadManyById

      orderPiecesForWidget(widget) {
        if (widget.by !== 'id') {
          return;
        }
        let key = self.joinById.relationship ? 'item._id' : '_id';
        widget._pieces = self.apos.utils.orderById(widget.pieceIds, widget._pieces, key);
      },

      // A utility method to append a piece to the ._pieces array for the given widget correctly,
      // whether the join has relationship properties or not.

      pushPieceForWidget(widget, piece) {
        if (self.joinById && self.joinById.relationship && widget.by === 'id') {
          widget._pieces.push({
            item: piece,
            relationship: (widget[self.joinById.relationshipsField] || {})[piece._id]
          });
        } else {
          widget._pieces.push(piece);
        }
      },

      // This ALWAYS gets called, so you can do special handling here no matter what.
      // Whether your widgets were loaded en masse "by id" or one at a time, this method
      // always gets called for each. Override this method to do whatever you want.

      async afterLoadOne(req, widget) {
      },

      // Hook to modify cursor before the load method is invoked. Applies the filters
      // specified for the join.

      widgetCursor(req, criteria) {
        const filters = self.filters || (self.joinById && self.joinById.filters);
        return self.pieces.find(req, criteria).applyBuilders(filters);
      },

      // Returns true if the widget is considered empty.

      isEmpty(widget) {
        if (widget._pieces) {
          return !widget._pieces.length;
        }
        return false;
      }
    };
  },
  extendMethods(self, options) {
    return {
<commit_msg>widgets faking a join should handle relationships the new way, just like normal join loading<commit_after>// `@apostrophecms/pieces-widget-type` provides widgets that display display pieces of a
// particular type around the site.
//
// You will `extend` this module in new modules corresponding to your modules
// that extend `@apostrophecms/piece-type`.
//
// To learn more and see complete examples, see:
//
// [Reusable content with pieces](../../tutorials/getting-started/reusable-content-with-pieces.html)
//
// ## options
//
// ### `loadManyById`
//
// If `true` (the default), Apostrophe will take all the widgets passed to the `load` method for which pieces
// were chosen "by id", i.e. manually in a join, and do a single query to fetch them. This is usually more efficient,
// however if you have customized the `loadOne` method you may not wish to use it.

const _ = require('lodash');

module.exports = {
  extend: '@apostrophecms/widget-type',
  options: {
    loadManyById: true,
    builders: {}
  },
  beforeSuperClass(self, options) {
    self.piecesModuleName = options.piecesModuleName || self.__meta.name.replace(/-widgets$/, '');

    self.pieces = options.apos.modules[self.piecesModuleName];
    if (!self.pieces) {
      if (!options.piecesModuleName) {
        throw new Error('The module ' + self.__meta.name + ' extends @apostrophecms/pieces-widget-type, but the piecesModuleName option is not set, and we can\'t guess it from the name of your module.');
      } else {
        throw new Error('The module ' + self.__meta.name + ' has a piecesModuleName option that does not match any existing module.');
      }
    }
    options.label = options.label || self.pieces.pluralLabel;
    // A pieces widget generally doesn't edit things when you click "Edit,"
    // it selects things (although you can create and select things, or
    // edit and then select things)
    self.editLabel = options.editLabel || 'Select ' + options.label;
    let by;
    if (options.by) {
      by = options.by;
    } else {
      by = [
        'all',
        'id'
      ];
    }

    let addFields = [];

    let byChoicesInfo = {
      id: {
        value: 'id',
        label: 'Individually',
        showFields: ['_pieces']
      },
      all: {
        value: 'all',
        label: options.byAllLabel || 'All',
        showFields: ['limitByAll']
      }
    };

    let byChoices = _.map(_.filter(by, function (source) {
      return _.has(byChoicesInfo, source);
    }), function (source) {
      return byChoicesInfo[source];
    });

    addFields.push({
      type: 'select',
      name: 'by',
      // This just keeps the words "id", "all" and "tag" from cluttering
      // up the search index. It has NO impact on the searchability
      // of the pieces. -Tom
      searchable: false,
      label: 'Select...',
      def: byChoices[0] && byChoices[0].value,
      choices: byChoices,
      contextual: byChoices.length <= 1
    });

    if (_.includes(by, 'all')) {
      addFields.push({
        type: 'integer',
        name: 'limitByAll',
        label: 'Maximum displayed',
        def: 5
      });
    }

    if (_.includes(by, 'id')) {
      addFields.push({
        type: 'joinByArray',
        name: '_pieces',
        label: byChoices.length > 1 ? 'Individually' : 'Select...',
        idsField: 'pieceIds',
        withType: self.pieces.name
      });
    }

    options.addFields = addFields.concat(options.addFields || []);

    options.browser = _.defaults(options.browser || {}, { piecesModuleName: self.piecesModuleName });
  },
  init(self, options) {

    self.filters = options.filters;
  },
  methods(self, options) {
    return {

      // Load the appropriate pieces for each widget in the array. Apostrophe will try to feed
      // us as many at once as it can to cut down on database queries. We'll take all the
      // widgets for which pieces were chosen "by id" and do a single query, via
      // self.loadManyById. For everything we'll call self.loadOne individually, via
      // self.loadOthersOneAtATime. But in ALL cases, we invoke self.afterLoadOne for
      // each widget, allowing an opportunity to do custom work without thinking
      // about all this.
      //
      // Also in all cases, joins found in the schema other than the `_pieces` join
      // are loaded in the normal way for each widget.

      async load(req, widgets) {
        // Since we are overriding the default load method of `@apostrophecms/widget-type`, make
        // sure we still implement the `scene` option for assets

        if (self.options.scene) {
          req.scene = self.options.scene;
        }

        if (!self.options.loadManyById) {
          // Carrying out one big query for all widgets that select pieces by id has been
          // disabled for this module
          await self.loadOthersOneAtATime(req, widgets);
          return self.loadOtherJoins(req, widgets);
        }
        const byId = _.filter(widgets, { by: 'id' });
        const byOther = _.difference(widgets, byId);
        await self.loadManyById(req, byId);
        await self.loadOthersOneAtATime(req, byOther);
        return self.loadOtherJoins(req, widgets);
      },

      // Load many widgets, all of which were set to choose pieces "by id." This allows
      // Apostrophe to work efficiently when a page contains many pieces widgets in an
      // array, etc. This method is called by self.load, you don't need to call it yourself.
      //
      // This method still calls afterLoadOne for each widget, so there is still a simple
      // way to go beyond this if you need to do something fancy after a widget has been
      // through the normal loading process.

      async loadManyById(req, widgets) {

        const ids = _.reduce(widgets, function (ids, widget) {
          return ids.concat(widget.pieceIds || []);
        }, []);

        const widgetsByPieceId = {};

        _.each(widgets, function (widget) {
          widget._pieces = [];
          widget._piecesById = {};
          _.each(widget.pieceIds || [], function (_id) {
            if (!_.has(widgetsByPieceId, _id)) {
              widgetsByPieceId[_id] = [];
            }
            widgetsByPieceId[_id].push(widget);
          });
        });

        const cursor = self.widgetCursor(req, { _id: { $in: ids } });

        const pieces = await cursor.toArray();

        _.each(pieces, function (piece) {
          if (_.has(widgetsByPieceId, piece._id)) {
            _.each(widgetsByPieceId[piece._id], function (widget) {
              if (!_.has(widget._piecesById, piece._id)) {
                self.pushPieceForWidget(widget, piece);
                widget._piecesById[piece._id] = true;
              }
            });
          }
        });

        // Make sure pieces are returned in the order they appear in the list chosen by the user
        _.each(widgets, function (widget) {
          self.orderPiecesForWidget(widget);
        });

        // Give every widget a chance to have special sauce in its afterLoadOne method
        for (const widget of widgets) {
          await self.afterLoadOne(req, widget);
        }
      },

      // Load widgets that were NOT set to choose pieces "by id." Feeds them all
      // through self.loadOne and self.afterLoadOne. You don't have to call this,
      // self.load calls it for you.

      async loadOthersOneAtATime(req, widgets) {
        for (const widget of widgets) {
          await self.loadOne(req, widget);
          await self.afterLoadOne(req, widget);
        }
      },

      // Load related content for a single widget. This method is invoked only
      // if the `loadManyById` option has been set to `false` for your subclass.
      // Doing so has performance costs when widgets are numerous.

      async loadOne(req, widget) {
        // Go get stuff by tag / by all, with limit, attach to widget, then...
        const cursor = self.widgetCursor(req, {});
        if (widget.by === 'all') {
          // We are interested in everything
          cursor.limit(widget.limitByAll || 5);
        } else if (widget.by === 'id') {
          // By default, "by id" goes through a separate path, but support it here
          // so that the loadOne method can be used directly and naively to load
          // a widget, no matter how it is set up. Also useful if the loadManyById
          // option is disabled
          cursor.and({ _id: { $in: widget.pieceIds || [] } });
        }
        const pieces = await cursor.toArray();
        self.attachPiecesToWidget(widget, pieces);
      },

      // Load additional joins in the schema, if any, beyond the `_pieces`
      // join that we have implemented ourselves with higher performance

      async loadOtherJoins(req, widgets) {
        const schema = _.filter(self.schema, function (field) {
          return field.type.match(/^join/) && field.name !== '_pieces';
        });
        return self.apos.schemas.join(req, schema, widgets, undefined);
      },

      // Given an array of pieces, this method attaches them to the widget
      // as the _pieces property correctly with pushPiecesToWidget, and
      // orders them correctly if the user chose them in a specific order

      attachPiecesToWidget(widget, pieces) {
        widget._pieces = [];
        _.each(pieces, function (piece) {
          self.pushPieceForWidget(widget, piece);
        });
        self.orderPiecesForWidget(widget);
      },

      // A utility method that puts the pieces loaded for the widget in the
      // order requested by the user. widget._pieces should already be loaded
      // at this point. Called for you by the widget loader methods; useful
      // if you are overriding loadOne and disabling loadManyById

      orderPiecesForWidget(widget) {
        if (widget.by !== 'id') {
          return;
        }
        let key = self.joinById.relationship ? 'item._id' : '_id';
        widget._pieces = self.apos.utils.orderById(widget.pieceIds, widget._pieces, key);
      },

      // A utility method to append a piece to the ._pieces array for the given widget correctly

      pushPieceForWidget(widget, piece) {
        if (self.joinById && self.joinById.relationship && widget.by === 'id') {
          piece._relationship = widget[self.joinById.relationshipsField] || {};
        }
        widget._pieces.push(piece);
      },

      // This ALWAYS gets called, so you can do special handling here no matter what.
      // Whether your widgets were loaded en masse "by id" or one at a time, this method
      // always gets called for each. Override this method to do whatever you want.

      async afterLoadOne(req, widget) {
      },

      // Hook to modify cursor before the load method is invoked. Applies the filters
      // specified for the join.

      widgetCursor(req, criteria) {
        const filters = self.filters || (self.joinById && self.joinById.filters);
        return self.pieces.find(req, criteria).applyBuilders(filters);
      },

      // Returns true if the widget is considered empty.

      isEmpty(widget) {
        if (widget._pieces) {
          return !widget._pieces.length;
        }
        return false;
      }
    };
  },
  extendMethods(self, options) {
    return {

{'fraction_non_alphanumeric': 0.10878605556538666, 'fraction_numerical': 0.0005308794903556893, 'mean_word_length': 2.234368293031907, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, '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': '2976829', 'n_tokens_mistral': 6431, 'n_tokens_neox': 6148, 'n_words': 2191}