ReactiveAggregate () + collection.update () -> Ошибка: ожидается, что документ будет изменен - PullRequest
0 голосов
/ 09 июня 2019

Реактивная агрегация изначально публикуется на клиенте без ошибок. Кажется, ошибка возникает, когда коллекция Meteor.user обновляется клиентом Meteor.call():

updateProductFavorites = (product_key, action) => {
    const { ranking } = this.props
    const { product_keys } = ranking[0]
    Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
        if (err)
            makeAlert(err.reason, 'danger', 3000)
        else 
            this.getProductsByKeys(product_keys)    
    })
}

Я подписался как на Meteor.user(), так и на реактивную агрегацию:

export default withTracker(() => {
    const handle = Meteor.subscribe("products.RankingList")
    return {
        ranking: AggregatedProductRanking.find({}).fetch(),
        user: Meteor.user(),
        isLoading: !handle.ready() || !Meteor.user()
    }
})(ProductRankingList)

Я объявил и импортировал clientCollection с обеих сторон, как также предлагается в этом ответе . Это соответствующий код на стороне сервера:

const getProductRankingList = (context) => ReactiveAggregate(context, Meteor.users, [
      // aggregation stages can be seen in the code snippet below
    ], { clientCollection: "aggregatedProductRanking"})

Meteor.methods({
    'Accounts.updateProductFavorites': function(product_key, action) {
         allowOrDeny(this.userId)
         action = action == 'add' ? { $addToSet: { productFavorites: product_key }} : { $pull: { productFavorites: product_key }}
         return Meteor.users.update({_id: this.userId}, action)
     }
})

Meteor.publish('products.RankingList', function() {
    const callback = () => this.stop()
    allowOrDenySubscription(this.userId, callback)
    return getProductRankingList(this)
})

Что меня сбивает с толку, так это то, что update, вызываемый Meteor.call('Accounts.updateProductFavorites'), по-прежнему надежно выполняется, даже если выдается эта ошибка.

Таким образом, изменение вошедшего в систему Meteor.user() возвращается клиенту, и компонент перерисовывается. Кажется, только подписка ReactiveAggregate перестает работать. Выдается следующая ошибка, и я должен перезагрузить браузер, чтобы увидеть изменения в результате агрегации. (полная трассировка стека внизу)

Uncaught Error: Expected to find a document to change
    at Object.update (collection.js:207)
    at Object.store.<computed> [as update] (livedata_connection.js:310)
    ...

// In a certain case the error message is a bit different:
Exception in flushing DDP buffered writes: Error: Expected to find a document to change
    at Object.update (collection.js:207)
    at Object.store.<computed> [as update] (livedata_connection.js:310)
    ...

Я предполагаю, что update() вызывается ReactiveAggregate() для заполнения clientCollection. Но что я делаю не так?

Для более полного примера кода:

Серверная сторона

import { Meteor } from 'meteor/meteor'
import { ReactiveAggregate } from 'meteor/jcbernack:reactive-aggregate';

// Collections
import { AggregatedProductRanking } from '../imports/collections'

const getProductRankingList = (context) => ReactiveAggregate(context, Meteor.users, [
        {
            $match: { productFavorites: {$ne: [] }}
        },{
            $project: {
                _id: 0,
                productFavorites: { $concatArrays: "$productFavorites" },
            }
        },{
            $unwind: "$productFavorites"
        },{
            $facet: {
                rankingList: [
                    {
                        $group: {
                            _id: "$productFavorites",
                            count: { $sum: 1 }
                        }
                    },{
                        $sort: { "count": -1 }
                    }
                ],
                product_keys: [
                    {
                        $group: { 
                            _id: 0,
                            product_keys: { $addToSet: "$productFavorites" }
                        }
                    }
                ]
            }
        },{
            $unwind: "$product_keys"
        },{
            $project: {
                _id: 0,
                rankingList: 1,
                product_keys: "$product_keys.product_keys"
            }
        }
    ], { clientCollection: "aggregatedProductRanking"})

Meteor.methods({
    'Accounts.updateProductFavorites': function(product_key, action) {
        allowOrDeny(this.userId)
        action = action == 'add' ? { $addToSet: { productFavorites: product_key }} : { $pull: { productFavorites: product_key }}
        return Meteor.users.update({_id: this.userId}, action)
    },
    'Products.getByProductKey': function(productFavorites) {
        allowOrDeny(this.userId)
        if (productFavorites == undefined)
            productFavorites = Meteor.users.findOne({_id: this.userId}, {fields: {productFavorites: 1}}).productFavorites
        if (productFavorites.length > 0) {
            return Products.find(
                { product_key: {$in: productFavorites }, price_100_g_ml: {$ne: null} },
                { sort: {product_name: 1} }).fetch()
        } else
            return []
    },
})

function allowOrDenySubscription(userId, callback) {
    if (!userId) {
        callback()
        return;
    }
}

Meteor.publish(null, function() {
    if (!this.userId)
        return false
    return Meteor.users.find({_id: this.userId}, { fields: {
        firstName: 1, lastName: 1,
        zip: 1, city: 1, street: 1, houseNumber: 1,
        phone: 1, iban: 1, bic: 1,
        memberId: 1, membershipFee: 1,
        productFavorites: 1
    }})
}, { is_auto: true })

Meteor.publish('products.RankingList', function() {
    const callback = () => this.stop()
    allowOrDenySubscription(this.userId, callback)
    return getProductRankingList(this)
})

Клиентская сторона

import { Meteor } from 'meteor/meteor';
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
// ... more imports

// Collections
import { AggregatedProductRanking } from '../../../imports/collections';


class ProductRankingList extends Component {
    constructor() {
        super()

        this.state = {
            products: [],
            productDetails: true,
            singleProductDetails: 0,
        }
    }

    getProductsByKeys = (product_keys) => {
        Meteor.call('Products.getByProductKey', product_keys, (err, response) => {
            if (err)
                makeAlert(err.reason, 'danger', 3000)
            else {
                this.setState({products: response})
                console.log(response)
            }
        })
    }

    updateProductFavorites = (product_key, action) => {
        const { ranking } = this.props
        const { product_keys } = ranking[0]
        console.log(product_keys)
        Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
            if (err)
                makeAlert(err.reason, 'danger', 3000)
            else 
                this.getProductsByKeys(product_keys)    
        })
    }

    toggleProductFavorite = (product_key) => {
        const { productFavorites } = this.props.user
        if (productFavorites.includes(product_key))
            this.updateProductFavorites(product_key, 'remove')
        else
            this.updateProductFavorites(product_key, 'add')
    }

    mapProductFavorites = () => {
        const { products, productDetails, singleProductDetails } = this.state
        const { productFavorites } = this.props.user
        const { ranking } = this.props
        const { rankingList } = ranking[0]

        if (products.length == 0)
            return <div className="alert alert-primary col-12">No one has favorited any products at the moment, it seems.</div>

        products.map((product, i) => {
            const { order_number, supplierId } = product
            product["count"] = rankingList.find(product => product._id == `${supplierId}_${order_number}`).count
        })

        products.sort((a, b) => b.count - a.count)

        return (
            products.map((product, i) => {
                if (product.price_100_g_ml) {
                    var [euro, cent] = product.price_100_g_ml.toFixed(2).toString().split('.')
                }

            const { product_name, units, trading_unit, certificate, origin, order_number, supplierId, count } = product
            const isFavorite = productFavorites.includes(`${supplierId}_${order_number}`) ? 'is-favorite' : 'no-favorite'

            return (
                <div className="col-lg-6" key={i}>
                    <div key={i} className="product-card">
                        <div className="card-header" onClick={() => this.toggleSingleProductDetails(order_number)}>
                            {product_name}
                            {/* <span className="fa-layers fa-fw heart-with-count">
                                <FontAwesomeIcon icon="heart"/>
                                <div className="fa-layers-text">{count}</div>
                            </span> */}
                        </div>
                        {productDetails || singleProductDetails == order_number ?
                        <>
                            <div className="card-body">
                                {euro ?
                                    <>
                                        <div className="product-actions">
                                            <button className={`btn btn-light btn-lg product-${isFavorite}`}
                                                onClick={() => this.toggleProductFavorite(`${supplierId}_${order_number}`)}>
                                                <FontAwesomeIcon icon="heart"/>
                                                <span className="ml-2">{count}</span>
                                            </button>
                                        </div>
                                        <div className="price-100-g-ml">
                                            <small>pro 100{units == 'kg' ? 'g' : 'ml'}</small><sup></sup>
                                            <big>{euro}</big>.<sup>{cent.substring(0,2)}</sup>
                                        </div>
                                    </> : null}
                            </div>
                            <div className="card-footer">
                                <div className="row">
                                    <div className="col-4">{trading_unit}</div>
                                    <div className="col-4 text-center">{certificate}</div>
                                    <div className="col-4 text-right">{origin}</div>
                                </div>                            
                            </div>
                        </> : null }
                    </div>
                </div>)
            })
        )
    }

    componentDidUpdate(prevProps, prevState) {
        const { isLoading, ranking } = this.props
        if (ranking.length < 1)
            return null
        if (!isLoading && (prevProps.ranking != ranking)) {
            this.getProductsByKeys(ranking[0].product_keys)
        }
    }

    render() {
        const { isLoading, ranking } = this.props
        if (isLoading || ranking.length < 1)
            return null

        return(
            <div className="row mt-3">
                {this.mapProductFavorites()}
            </div>
        )
    }
}

export default withTracker(() => {
    const handle = Meteor.subscribe("products.RankingList")
    return {
        ranking: AggregatedProductRanking.find({}).fetch(),
        user: Meteor.user(),
        isLoading: !handle.ready() || !Meteor.user()
    }
})(ProductRankingList)

Полная трассировка стека

// When loading component through history.push()
Exception in flushing DDP buffered writes: Error: Expected to find a document to change
    at Object.update (collection.js:207)
    at Object.store.<computed> [as update] (livedata_connection.js:310)
    at livedata_connection.js:1192
    at Array.forEach (<anonymous>)
    at livedata_connection.js:1191
    at Array.forEach (<anonymous>)
    at Connection._performWrites (livedata_connection.js:1187)
    at Connection._flushBufferedWrites (livedata_connection.js:1167)
    at meteor.js?hash=857dafb4b9dff17e29ed8498a22ea5b1a3d6b41d:1234
    
    
// After second call of Meteor.call('Accounts.updateProductFavorites')
Uncaught Error: Expected to find a document to change
    at Object.update (collection.js:207)
    at Object.store.<computed> [as update] (livedata_connection.js:310)
    at livedata_connection.js:1192
    at Array.forEach (<anonymous>)
    at livedata_connection.js:1191
    at Array.forEach (<anonymous>)
    at Connection._performWrites (livedata_connection.js:1187)
    at Connection._flushBufferedWrites (livedata_connection.js:1167)
    at Connection._livedata_data (livedata_connection.js:1133)
    at Connection.onMessage (livedata_connection.js:1663)

1 Ответ

0 голосов
/ 10 июня 2019

Оказалось, что решение было довольно простым: причиной вышеупомянутых ошибок было пропущенное _id поле в результате агрегирования:

    ...
    },{
            $project: {
                _id: 0, // This will give the error.
                rankingList: 1,
                product_keys: "$product_keys.product_keys"
            }
        }
    ], { clientCollection: "aggregatedProductRanking"})

Документыдля ReactiveAggregate() ( метеоритно-реактивный агрегат ) указывается, что поле _id может быть опущено, так как оно будет автоматически создано ReactiveAggregate().Но даже после удаления _id: 0, это не сработало.

Что работает так:

    ...
    },{
            $project: {
                _id: "whatTheFuckIsGoingOnHere", // Calm down, bro!
                rankingList: 1,
                product_keys: "$product_keys.product_keys"
            }
        }
    ], { clientCollection: "aggregatedProductRanking"})

Удивительно реактивные агрегации, только на немногоболь в а **.Я сделал отчет об ошибке в репозитории github

...