<commit_before>// Load modules

import {
    assign as _assign,
    forEach as _forEach,
    has as _has,
    includes as _includes,
    isEmpty as _isEmpty,
    isArray as _isArray,
    isObject as _isObject,
    isObjectLike as _isObjectLike,
    keys as _keys,
    map as _map,
    zipObject as _zipObject
} from 'lodash';

import Paginator from 'bookshelf-page';

/**
 * Exports a plugin to pass into the bookshelf instance, i.e.:
 *
 *      import config from './knexfile';
 *      import knex from 'knex';
 *      import bookshelf from 'bookshelf';
 *
 *      const Bookshelf = bookshelf(knex(config));
 *
 *      Bookshelf.plugin('bookshelf-jsonapi-params');
 *
 *      export default Bookshelf;
 *
 * The plugin attaches the `fetchJsonApi` instance method to
 * the Bookshelf Model object.
 *
 * See methods below for details.
 */
export default (Bookshelf, options = {}) => {

    // Load the pagination plugin
    Bookshelf.plugin(Paginator);

    /**
     * Similar to {@link Model#fetch} and {@link Model#fetchAll}, but specifically
     * uses parameters defined by the {@link https://jsonapi.org|JSON API spec} to
     * build a query to further refine a result set.
     *
     * @param  opts {object}
     *     Currently supports the `include`, `fields`, `sort`, `page` and `filter`
     *     parameters from the {@link https://jsonapi.org|JSON API spec}.
     * @param  type {string}
     *     An optional string that specifies the type of resource being retrieved.
     *     If not specified, type will default to the name of the table associated
     *     with the model.
     * @return {Promise<Model|Collection|null>}
     */
    const fetchJsonApi = function (opts, isCollection = true, type) {

        opts = opts || {};

        const internals = {};
        const { include, fields, sort, page = {}, filter } = opts;

        // Get a reference to the field being used as the id
        internals.idAttribute = this.constructor.prototype.idAttribute ?
            this.constructor.prototype.idAttribute : 'id';

        // Get a reference to the current model name. Note that if no type is
        // explcitly passed, the tableName will be used
        internals.modelName = type ? type : this.constructor.prototype.tableName;

        // Initialize an instance of the current model and clone the initial query
        internals.model =
            this.constructor.forge().query((qb) => _assign(qb, this.query().clone()));

        /**
         * Build a query based on the `fields` parameter.
         * @param  fieldNames {object}
         */
        internals.buildFields = (fieldNames = {}) => {

            if (_isObject(fieldNames) && !_isEmpty(fieldNames)) {

                // Format column names
                fieldNames = internals.formatColumnNames(fieldNames);

                // Process fields for each type/relation
                _forEach(fieldNames, (value, key) => {

                    // Add qualifying table name to avoid ambiguous columns
                    fieldNames[key] = _map(fieldNames[key], (value) => {

                        return `${key}.${value}`;
                    });

                    // Only process the field if it's not a relation. Fields
                    // for relations are processed in `buildIncludes()`
                    if (!_includes(include, key)) {

                        // Add column to query
                        internals.model.query((qb) => {

                            qb.column.apply(qb, [value]);

                            // JSON API considers relationships as fields, so we
                            // need to make sure the id of the relation is selected
                            _forEach(include, (relation) => {

                                const relationId = `${relation}_id`;

                                if (!internals.isManyRelation(relation) &&
                                    !_includes(fieldNames[relation], relationId)) {
                                    qb.column.apply(qb, [relationId]);
                                }
                            });
                        });
                    }
                });
            }
        };

        /**
         * Build a query based on the `filters` parameter.
         * @param  filterValues {object|array}
         */
        internals.buildFilters = (filterValues) => {

            if (_isObjectLike(filterValues) && !_isEmpty(filterValues)) {

                // format the column names of the filters
                filterValues = this.format(filterValues);

                // build the filter query
                internals.model.query((qb) => {

                    _forEach(filterValues, (value, key) => {

                        // Determine if there are multiple filters to be applied
                        value = value.toString().indexOf(',') !== -1 ? value.split(',') : value;

                        qb.whereIn.apply(qb, [key, value]);
                    });
                });
            }
        };

        /**
         * Build a query based on the `include` parameter.
         * @param  includeValues {array}
         */
        internals.buildIncludes = (includeValues) => {

            if (_isArray(includeValues) && !_isEmpty(includeValues)) {

                const relations = [];

                _forEach(includeValues, (relation) => {

                    if (_has(fields, relation)) {

                        const fieldNames = internals.formatColumnNames(fields);

                        relations.push({
                            [relation]: (qb) => {

                                const relationId = `${internals.modelName}_id`;

                                if (!internals.isBelongsToRelation(relation) &&
                                    !_includes(fieldNames[relation], relationId)) {

                                    qb.column.apply(qb, [relationId]);
                                }

                                qb.column.apply(qb, [fieldNames[relation]]);
                            }
                        });
                    }
                    else {
                        relations.push(relation);
                    }
                });

                _assign(opts, { withRelated: relations });
            }
        };

        /**
         * Build a query based on the `sort` parameter.
         * @param  sortValues {array}
         */
        internals.buildSort = (sortValues = []) => {

            if (_isArray(sortValues) && !_isEmpty(sortValues)) {

                sortValues = internals.formatColumnNames(sortValues);

                _forEach(sortValues, (sortBy) => {

                    internals.model.orderBy(sortBy);
                });
            }
        };

        /**
         * Processes incoming parameters that represent columns names and
         * formats them using the internal {@link Model#format} function.
         * @param  columnNames {array}
         * @return {array{}
         */
        internals.formatColumnNames = (columnNames = []) => {

            _forEach(columnNames, (value, key) => {

                let columns;

                // Convert column names to an object so it can
                // be passed to Model#format
                if (_isArray(columnNames[key])) {
                    columns = _zipObject(columnNames[key], null);
                }
                else {
                    columns = _zipObject(columnNames, null);
                }

                // Re-add idAttribute as it's required by the JSONAPI spec<commit_msg>Fix descending sort support<commit_after>// Load modules

import {
    assign as _assign,
    forEach as _forEach,
    has as _has,
    includes as _includes,
    isEmpty as _isEmpty,
    isArray as _isArray,
    isObject as _isObject,
    isObjectLike as _isObjectLike,
    keys as _keys,
    map as _map,
    zipObject as _zipObject
} from 'lodash';

import Paginator from 'bookshelf-page';

/**
 * Exports a plugin to pass into the bookshelf instance, i.e.:
 *
 *      import config from './knexfile';
 *      import knex from 'knex';
 *      import bookshelf from 'bookshelf';
 *
 *      const Bookshelf = bookshelf(knex(config));
 *
 *      Bookshelf.plugin('bookshelf-jsonapi-params');
 *
 *      export default Bookshelf;
 *
 * The plugin attaches the `fetchJsonApi` instance method to
 * the Bookshelf Model object.
 *
 * See methods below for details.
 */
export default (Bookshelf, options = {}) => {

    // Load the pagination plugin
    Bookshelf.plugin(Paginator);

    /**
     * Similar to {@link Model#fetch} and {@link Model#fetchAll}, but specifically
     * uses parameters defined by the {@link https://jsonapi.org|JSON API spec} to
     * build a query to further refine a result set.
     *
     * @param  opts {object}
     *     Currently supports the `include`, `fields`, `sort`, `page` and `filter`
     *     parameters from the {@link https://jsonapi.org|JSON API spec}.
     * @param  type {string}
     *     An optional string that specifies the type of resource being retrieved.
     *     If not specified, type will default to the name of the table associated
     *     with the model.
     * @return {Promise<Model|Collection|null>}
     */
    const fetchJsonApi = function (opts, isCollection = true, type) {

        opts = opts || {};

        const internals = {};
        const { include, fields, sort, page = {}, filter } = opts;

        // Get a reference to the field being used as the id
        internals.idAttribute = this.constructor.prototype.idAttribute ?
            this.constructor.prototype.idAttribute : 'id';

        // Get a reference to the current model name. Note that if no type is
        // explcitly passed, the tableName will be used
        internals.modelName = type ? type : this.constructor.prototype.tableName;

        // Initialize an instance of the current model and clone the initial query
        internals.model =
            this.constructor.forge().query((qb) => _assign(qb, this.query().clone()));

        /**
         * Build a query based on the `fields` parameter.
         * @param  fieldNames {object}
         */
        internals.buildFields = (fieldNames = {}) => {

            if (_isObject(fieldNames) && !_isEmpty(fieldNames)) {

                // Format column names
                fieldNames = internals.formatColumnNames(fieldNames);

                // Process fields for each type/relation
                _forEach(fieldNames, (value, key) => {

                    // Add qualifying table name to avoid ambiguous columns
                    fieldNames[key] = _map(fieldNames[key], (value) => {

                        return `${key}.${value}`;
                    });

                    // Only process the field if it's not a relation. Fields
                    // for relations are processed in `buildIncludes()`
                    if (!_includes(include, key)) {

                        // Add column to query
                        internals.model.query((qb) => {

                            qb.column.apply(qb, [value]);

                            // JSON API considers relationships as fields, so we
                            // need to make sure the id of the relation is selected
                            _forEach(include, (relation) => {

                                const relationId = `${relation}_id`;

                                if (!internals.isManyRelation(relation) &&
                                    !_includes(fieldNames[relation], relationId)) {
                                    qb.column.apply(qb, [relationId]);
                                }
                            });
                        });
                    }
                });
            }
        };

        /**
         * Build a query based on the `filters` parameter.
         * @param  filterValues {object|array}
         */
        internals.buildFilters = (filterValues) => {

            if (_isObjectLike(filterValues) && !_isEmpty(filterValues)) {

                // format the column names of the filters
                filterValues = this.format(filterValues);

                // build the filter query
                internals.model.query((qb) => {

                    _forEach(filterValues, (value, key) => {

                        // Determine if there are multiple filters to be applied
                        value = value.toString().indexOf(',') !== -1 ? value.split(',') : value;

                        qb.whereIn.apply(qb, [key, value]);
                    });
                });
            }
        };

        /**
         * Build a query based on the `include` parameter.
         * @param  includeValues {array}
         */
        internals.buildIncludes = (includeValues) => {

            if (_isArray(includeValues) && !_isEmpty(includeValues)) {

                const relations = [];

                _forEach(includeValues, (relation) => {

                    if (_has(fields, relation)) {

                        const fieldNames = internals.formatColumnNames(fields);

                        relations.push({
                            [relation]: (qb) => {

                                const relationId = `${internals.modelName}_id`;

                                if (!internals.isBelongsToRelation(relation) &&
                                    !_includes(fieldNames[relation], relationId)) {

                                    qb.column.apply(qb, [relationId]);
                                }

                                qb.column.apply(qb, [fieldNames[relation]]);
                            }
                        });
                    }
                    else {
                        relations.push(relation);
                    }
                });

                _assign(opts, { withRelated: relations });
            }
        };

        /**
         * Build a query based on the `sort` parameter.
         * @param  sortValues {array}
         */
        internals.buildSort = (sortValues = []) => {
            const sortDirection = {};

            if (_isArray(sortValues) && !_isEmpty(sortValues)) {

                for (let i = 0; i < sortValues.length; i++) {
                    if (typeof sortValues[i] === 'string' && sortValues[i].startsWith('-')) {
                        sortValues[i] = sortValues[i].substring(1, sortValues[i].length);
                        sortDirection[sortValues[i]] = 'desc';
                    }
                }

                sortValues = internals.formatColumnNames(sortValues);

                _forEach(sortValues, (sortBy) => {
                    internals.model.orderBy(sortBy, typeof sortDirection[sortBy] === 'undefined' ? 'asc' : sortDirection[sortBy]);
                });
            }
        };

        /**
         * Processes incoming parameters that represent columns names and
         * formats them using the internal {@link Model#format} function.
         * @param  columnNames {array}
         * @return {array{}
         */
        internals.formatColumnNames = (columnNames = []) => {

            _forEach(columnNames, (value, key) => {

                let columns;

                // Convert column names to an object so it can
                // be passed to Model#format
                if (_isArray(columnNames[key])) {
                    columns = _zipObject(columnNames[key], null);
                }
                else {
                    columns = _zipObject(columnNames, null);
                }

                // Re-add idAttribute as it's required by the JSONAPI spec
{'fraction_non_alphanumeric': 0.10307898259705489, 'fraction_numerical': 0.0002549882067954357, 'mean_word_length': 1.4322480620155038, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 39, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3250213', 'n_tokens_mistral': 3974, 'n_tokens_neox': 3702, 'n_words': 1074}