Search code examples
rubyreactjsgraphqlrelayjs

Relay re-fetching failed... error Relay was unable to reconcile edges on a connection


Help, plz! Perhaps someone already faced with my problem....

When re-fetch data with the updated variables -- using relay.setVariables() -- I get the error "Relay was unable to reconcile edges on a connection. This most likely occurred while trying to handle a server response that includes connection edges with nodes that lack anid field." and Relay storage is not updated although the data come correct.

See code below... (Ruby && ES6)

gemfile

gem 'graphql', '0.16.0'
gem 'graphql-relay', '0.11.2'

....server-side code

node_identification.rb

NodeIdentification = GraphQL::Relay::GlobalNodeIdentification.define do
object_from_id -> (id, ctx) do
    type, id = NodeIdentification.from_global_id(id)
    case type
        when 'FrontApp'
            Relay::FrontApp::STATIC
        else
            Object.const_get(type).find_by(id: id)
    end
end

type_from_object -> (obj) do
    begin
        MODEL_TO_TYPE[obj.class.name.to_sym].constantize
    rescue
        (obj.class.name + 'Type').constantize
    end
end
end

MODEL_TO_TYPE = {
   :'Relay::FrontApp' => 'FrontAppType'
}

front_app_query_type.rb

FrontAppQueryType = GraphQL::ObjectType.define do
name 'FrontAppRootQuery'
field :node, field: NodeIdentification.field

field :main, FrontAppType do
    resolve -> (obj, args, ctx) {
        Relay::FrontApp::STATIC
    }
end
end

front_app_type.rb

FrontAppType = GraphQL::ObjectType.define do
name 'FrontApp'
# field :node, field: NodeIdentification.field
interfaces [NodeIdentification.interface]
global_id_field :id

field :tips, TipsType do
    argument :filters, types.String
    resolve -> (obj, args, ctx) {
        filters = args[:filters]
        begin
            filters = JSON.parse(filters).deep_symbolize_keys!
        rescue
            filters = nil
        end
        ctx[:filters] = filters
        Relay::Tips::STATIC
    }
end
connection :footer, FooterMenuItemType.connection_type do
    argument :id, types.ID!
    resolve ->(obj, args, ctx){
        ::Footer.order(:id)
    }
end
end

tips_connection_type.rb

TipsConnectionType = TipShowType.define_connection do
field :totalCount, types.Int do
    resolve -> (obj, args, ctx) {
        obj.object.size
    }
end
end


TipsType = GraphQL::ObjectType.define do
name 'Tips'
description 'Tips list for home page'
interfaces [NodeIdentification.interface]
global_id_field :id

connection :mostRecent, TipsConnectionType do
    argument :limit, types.Int
    resolve ->(obj, args, ctx){
        tips = ::Tip.active_users.started(Time.zone.now.in_time_zone(ctx[:current_user] ? ctx[:current_user].time_zone : ::User.get_locally_time_zone).to_date).ready.active.moderated.published.includes(:comments, :tip_type).order('created_at desc').limit(args[:limit])
        TipHelpers::Filter.filter(tips: tips, filters: ctx[:filters], reorder: 'created_at desc')
    }
end
connection :mostPopular, TipsConnectionType do
    argument :limit, types.Int
    resolve ->(obj, args, ctx){
        tips = ::Tip.active_users.started(Time.zone.now.in_time_zone(ctx[:current_user] ? ctx[:current_user].time_zone : ::User.get_locally_time_zone).to_date).ready.active.moderated.published.includes(:comments, :tip_type).order('(SELECT COUNT(*) FROM comments WHERE tid = tips.id) desc').limit(args[:limit])
        TipHelpers::Filter.filter(tips: tips, filters: ctx[:filters], reorder: '(SELECT COUNT(*) FROM comments WHERE tid = tips.id) desc')
    }
end
end

/app/models/relay/front_app.rb

module Relay
class FrontApp < Struct.new :id
    # HACK::// For relay root queries
    STATIC = new(id: 'main').freeze

    def initialize *args
        opts = args.last.is_a?(Hash) ? args.pop : Hash.new
        super *args
        opts.each_pair do |k, v|
            self.send "#{k}=", v
        end
    end

    def self.find(_)
        STATIC
    end
end
end

** /app/models/relay/tips.rb**

module Relay
class Tips < Struct.new :id
    # HACK:// For relay root queries
    STATIC = new(id: 'tips').freeze

    def initialize *args
        opts = args.last.is_a?(Hash) ? args.pop : Hash.new
        super *args
        opts.each_pair do |k, v|
            self.send "#{k}=", v
        end
    end

    def self.find(_)
        STATIC
    end

end
end

....and client-side code

class MainApp extends React.Component {

constructor(props) {
    super(props);

    this.state = {
        filters: filtersTemplate
    };

    this.setFilter = this.setFilter.bind(this);
}

setFilter(filter, value, e) {
    if (e) {
        e.nativeEvent.stopImmediatePropagation();
        e.preventDefault();
    }

    let { filters } = this.state;
    if (['currency', 'sum'].inArray(filter)) {
        filters.funds[filter] = value;
    } else {
        filters[filter] = value;
    }
    this.setState({
        filters: filters
    });
    this.props.relay.setVariables({filters: filters});
}

render() {
    let { tips } = this.props.main;
    let renderTipsSection = (section) => {
        let tipsCount = tips[section] ? tips[section].edges.length : 0;
        let blankCount = 10 - tipsCount;
        return (
            <ul className="tips__list">
                {
                    tips[section] && tips[section].edges.map(({node}) => (
                        <li key={node.id} className="tips__list_item">
                            <TipCard node={node}/>
                        </li>
                    ))
                }
                {
                    [...new Array(blankCount).keys()].map((item, idx) => (
                        <li key={idx} className="tips__list_item">
                            <TipCard dummy={true}/>
                        </li>
                    ))
                }
            </ul>
        );
    };
    return (
        <div>
            <div className="wrapper">
                <div className="TipsWrapper">
                    <div className="wrapper">

                        { renderTipsSection('mostRecent') }

                        { renderTipsSection('mostPopular') }

                    </div>
                </div>
            </div>
        </div>
    );
}}

export default Relay.createContainer(MainApp, {
initialVariables: {
    filters: {
        category: null,
        funds: {
            currency: null,
            sum: null
        },
        date: null,
        location: null,
        browse: null
    }
},
prepareVariables: (prevVars) => {
    return {
        ...prevVars,
        filters: JSON.stringify(prevVars.filters)
    }
},
fragments: {
    main: () => Relay.QL`
        fragment on FrontApp {
            tips(filters: $filters) {
              mostRecent(first: 10, limit: 10){
                edges {
                  node {
                    id
                    tid
                    title_name
                    category
                  }
                }
              }  
              mostPopular(first: 10, limit: 10){
                edges {
                  node {
                    id
                    tid
                    title_name
                    category
                  }
                }
              }  
            }
        }
    `
}});

When setFilter() triggered the relay.setVariables will be called... and result...

[RELAY-NETWORK] Run query q3 Object {relayReqId: "q3", relayReqObj: RelayQueryRequest, relayReqType: "query", method: "POST", headers: Object…}
[RELAY-NETWORK] query q3: 3429ms
Warning: Relay was unable to reconcile edges on a connection. This most likely occurred while trying to handle a server response that includes connection edges with nodes that lack an `id` field

Solution

  • After many hours of torment solution of the problem above described was found... see below

    SOLUTION

    client-side mutation

    export default class ApplyFiltersMutation extends Relay.Mutation {
    static fragments = {
        tips: () => Relay.QL`
            fragment on Tips { 
                id 
            }
        `,
    };
    
    getMutation() {
        return Relay.QL`mutation {
            applyFilters
        }`;
    }
    
    getVariables() {
        return {
            filters: this.props.filters
        };
    }
    
    getFatQuery() {
        return Relay.QL`
            fragment on ApplyFiltersPayload {
                tips
            }
        `;
    }
    
    getConfigs() {
        return [
            {
                type: 'FIELDS_CHANGE',
                fieldIDs: {tips: this.props.tips.id},
            }
        ];
    }
    }
    

    server-side

    home_mutations.rb

    module HomeMutations
      ApplyFilters = GraphQL::Relay::Mutation.define do
        name 'ApplyFilters'
        input_field :filters, !types.String
    
        return_field :tips, BipsType
    
        resolve -> (args, ctx) {
            filters = args[:filters]
            begin
                filters = JSON.parse(filters).deep_symbolize_keys!
            rescue
                filters = nil
            end
            ctx[:filters] = filters
    
            {
                tips: Relay::Tips::STATIC
            }
        }
      end
    end
    

    tips_type.rb

    include TipHelpers::Filter
    
    TipsType = GraphQL::ObjectType.define do
    name 'Tips'
    description 'Tips list for home page'
    interfaces [NodeIdentification.interface]
    global_id_field :id
    
    connection :almostRaised, TipsConnectionType do
        resolve ->(obj, args, ctx){
            TipHelpers::Filter.filter(section: 'almost_raised', filters: ctx[:filters], current_user: ctx[:current_user])
        }
    end
    
    ...
    
    end
    end
    

    lib/tip_helpers.rb

    class TipHelpers
    module Filter
    
        def filter(section:, filters:, current_user:)
    
            ...
    
            # p tips.reorder(reorder).to_sql
            tips.reorder(reorder)
        end
    end
    end