Giter Club home page Giter Club logo

Comments (8)

visusnet avatar visusnet commented on July 28, 2024 1

The update changes the behavior of subscriptions. The new behavior is described here: https://aws.amazon.com/de/blogs/mobile/appsync-realtime/

The legacy AWS AppSync PubSub provider created MQTT connections via websockets. The new real-time AWS AppSync PubSub provider uses pure websockets. The legacy API was able to work with multiple root fields in subscriptions which are prohibited by GraphQL's spec (https://spec.graphql.org/June2018/#sec-Single-root-field). The pure websockets API seems to be more specification-compliant. Unfortunately, this breaks applications that rely on multi-field subscriptions.

I know you are using Vue but maybe you get (or anyone who stumbles upon this issue get) some inspiration to work around this problem with the solution that works for us: We created an adapted version of the Connect component (https://github.com/aws-amplify/amplify-js/blob/master/packages/aws-amplify-react/src/API/GraphQL/Connect.tsx) and implemented support for multiple subscriptions (and queries):

import { Component } from 'react';
import API from '@aws-amplify/api';
import merge from 'deepmerge';

function getNextToken(data) {
  const keyWithNextToken = Object.keys(data).find(key => data[key].nextToken);
  return (
    keyWithNextToken &&
    data[keyWithNextToken] &&
    data[keyWithNextToken].nextToken
  );
}

function recursiveQuery(operation) {
  return new Promise(resolve => {
    API.graphql(operation).then(response => {
      const nextToken = getNextToken(response.data);
      if (nextToken) {
        recursiveQuery({
          query: operation.query,
          variables: {
            ...operation.variables,
            nextToken
          }
        }).then(data => {
          resolve(merge(response.data, data));
        });
      } else {
        resolve(response.data);
      }
    });
  });
}

export default class Connect extends Component {
  constructor(props) {
    super(props);

    this.state = this.getInitialState();
    this.subSubscriptions = [];
  }

  getInitialState() {
    const { query } = this.props;
    return {
      loading: query && !!query.query,
      data: [],
      errors: [],
      mutation: () => console.warn('Not implemented')
    };
  }

  getDefaultState() {
    return {
      loading: false,
      data: [],
      errors: [],
      mutation: () => console.warn('Not implemented')
    };
  }

  async _fetchData() {
    this._unsubscribe();
    this.setState({ loading: true });

    let {
      query: { query: legacyQuery, variables: legacyVariables = {} } = {},
      queries,
      mutation: { query: mutation } = {},
      subscription: legacySubscription,
      subscriptions,
      onSubscriptionMsg = prevData => prevData
    } = this.props;

    let { data, mutation: mutationProp, errors } = this.getDefaultState();

    if (
      !API ||
      typeof API.graphql !== 'function' ||
      typeof API.getGraphqlOperationType !== 'function'
    ) {
      throw new Error(
        'No API module found, please ensure @aws-amplify/api is imported'
      );
    }

    if (legacySubscription) {
      subscriptions = [legacySubscription];
      console.warn(
        'subscription and subscriptions are specified. subscription is ignored.'
      );
    }
    let variables = [];
    if (legacyQuery) {
      queries = [legacyQuery];
      variables = [legacyVariables];
      console.warn('query and queries are specified. subscription is ignored.');
    } else {
      variables = queries.map(query => query.variables);
    }

    const hasValidQueries =
      queries &&
      queries.every(q => API.getGraphqlOperationType(q.query) === 'query');
    const hasValidMutation =
      mutation && API.getGraphqlOperationType(mutation) === 'mutation';
    const hasValidSubscriptions =
      subscriptions &&
      subscriptions.every(
        s => API.getGraphqlOperationType(s.query) === 'subscription'
      );

    if (!hasValidQueries && !hasValidMutation && !hasValidSubscriptions) {
      console.warn('No query, mutation or subscription was specified');
    }

    if (hasValidQueries) {
      data = [];
      queries.forEach((query, index) => {
        try {
          recursiveQuery({
            query: query.query,
            variables: variables[index]
          }).then(responseData => {
            data[index] = responseData;
            this.setState({ data });
          });
        } catch (err) {
          data[index] = err.data;
          errors[index] = err.errors;
        }
      });
    }

    if (hasValidMutation) {
      mutationProp = async variables => {
        const result = await API.graphql({
          query: mutation,
          variables: legacyVariables
        });

        this.forceUpdate();
        return result;
      };
    }
    if (hasValidSubscriptions) {
      subscriptions.forEach((subscription, index) => {
        const { query: subsQuery, variables: subsVars } = subscription;

        try {
          const observable = API.graphql({
            query: subsQuery,
            variables: subsVars
          });

          this.subSubscriptions.push(
            observable.subscribe({
              next: ({ value: { data } }) => {
                const { data: prevData } = this.state;
                const newData = onSubscriptionMsg(prevData, data);
                if (this.mounted) {
                  this.setState({ data: newData });
                }
              },
              error: err => console.error(err)
            })
          );
        } catch (err) {
          errors[index] = err.errors.concat(errors[index]);
        }
      });
    }

    this.setState({ data, errors, mutation: mutationProp, loading: false });
  }

  _unsubscribe() {
    this.subSubscriptions.forEach(subSubscription =>
      subSubscription.unsubscribe()
    );
  }

  async componentDidMount() {
    this._fetchData();
    this.mounted = true;
  }

  componentWillUnmount() {
    this._unsubscribe();
    this.mounted = false;
  }

  componentDidUpdate(prevProps) {
    const { loading } = this.state;

    const { query: newQueryObj, mutation: newMutationObj } = this.props;
    const { query: prevQueryObj, mutation: prevMutationObj } = prevProps;

    // quer
    const { query: newQuery, variables: newQueryVariables } = newQueryObj || {};
    const { query: prevQuery, variables: prevQueryVariables } =
      prevQueryObj || {};
    const queryChanged =
      prevQuery !== newQuery ||
      JSON.stringify(prevQueryVariables) !== JSON.stringify(newQueryVariables);

    // mutatio
    const { query: newMutation, variables: newMutationVariables } =
      newMutationObj || {};
    const { query: prevMutation, variables: prevMutationVariables } =
      prevMutationObj || {};
    const mutationChanged =
      prevMutation !== newMutation ||
      JSON.stringify(prevMutationVariables) !==
        JSON.stringify(newMutationVariables);

    if (!loading && (queryChanged || mutationChanged)) {
      this._fetchData();
    }
  }

  render() {
    const { data, loading, mutation, errors } = this.state;
    return this.props.children({ data, errors, loading, mutation }) || null;
  }
}

from amplify-ui.

amhinson avatar amhinson commented on July 28, 2024 1

As @visusnet pointed out, the new AppSync real-time endpoints do not support having multiple root fields in the subscription for the Connect component. However, there does appear to be a few workarounds noted in this issue. My suggestion would just be to manage each subscription directly with the API category (docs) or go with a custom solution provided here.

With that said, we are continuing build out our new UI Components, which provide a much more consistent experience across the major JavaScript frameworks. The Connect component is not supported there yet, but it is on the roadmap. I am marking this as a "feature request" for now to be sure this is taken into account in the future.

from amplify-ui.

my-name-is-nheo avatar my-name-is-nheo commented on July 28, 2024 1

Is there a way I can know the status of this issue? Using PubSub from an older version on aws-amplify 3.3.18. However updating to the latest version stops all my AWS services on the mobile end. Tried reconfiguring Amplify by adding my IAM credentials. So far, no success.

from amplify-ui.

michaelcuneo avatar michaelcuneo commented on July 28, 2024

I've used this version of 'Connect' to get my multiple subscriptions working, but it throws two warnings, then an error 'TypeError: Must provide source. Received: Undefined.

This is my connect...

            <Connect
              key="CategoriesConnector"
              query={graphqlOperation(listPosts, {
                limit: 100,
              })}
              subscription={graphqlOperation(onCreateOrUpdateOrDelete)}
              onSubscriptionMsg={(prev, {addedPost, editedPost, deletePost}) => {
                console.log(prev);
                if (addedPost) {
                    prev.listPosts.items.push(addedPost);
                } else if (editedPost) {
                    prev.listPosts.items = prev.listPosts.items.map(post => post.id === editedPost.id ? editedPost : post);
                } else if (deletePost) {
                    prev.listPosts.items = prev.listPosts.items.filter(post => post.id !== deletePost.id);
                }
                return prev;
              }}
            >
              {({ data, loading, error }) => {
                if (error) return <h3 key="Error">Error</h3>;
                if (loading || !data) return <h3>No Posts to load</h3>;
                return [
                  data.listPosts.items.map(item => (
                    <PostContainer key={item.id} post={item} />
                  ))
                ];
              }}
            </Connect>

And my custom subscriptions...

export const onCreateOrUpdateOrDelete = /* GraphQL */ `
  subscription OnCreateOrUpdateOrDelete {
    addedPost {
      id
      date
      data
    }
    editedPost {
      id
      date
      data
    }
    deletePost {
      id
      date
      data
    }
  }
`;



from amplify-ui.

Rolando-Barbella avatar Rolando-Barbella commented on July 28, 2024

I am having the same issue, can't do a simple crud with this component

from amplify-ui.

watanabethais avatar watanabethais commented on July 28, 2024

I'm having the same issue in React after migrating from "aws-amplify": "^1.1.32", "aws-amplify-react": "^2.3.11".

from amplify-ui.

davorian avatar davorian commented on July 28, 2024

I tried the Connect.tsx version above and got the same errors reported by others in this thread. So I created a version whereby you can pass in multiple subscriptions as an array - you can still pass in single a subscription too - as per the original version. Note: this version takes only a single query and a single onSubscriptionMessage - however your onSubscriptionMessage can be a wrapper function that examines the newData passed into it and calls the appropriate update depending on this data like this:

const onSubscriptionMessage = (prevQuery, newData) => {
    if(newData && newData.onDeleteItem) {
      return onRemoveItem(prevQuery, newData);
    }
    if(newData && newData.onCreateItem) {
      return onAddItem(prevQuery, newData);
    }
  };

Connect.tsx for multiple subscriptions given a single query and a single onSubscriptionMessage handler that switches handling according to the newData.

import * as React from 'react';
import { API, GraphQLResult } from '@aws-amplify/api';
import Observable from 'zen-observable-ts';

export interface IConnectProps {
    mutation?: any;
    onSubscriptionMsg?: (prevData: any) => any;
    query?: any;
    subscription?: any;
}

export interface IConnectState {
    loading: boolean;
    data: any;
    errors: any;
    mutation: any;
}

export class Connect extends React.Component<IConnectProps, IConnectState> {
    public subSubscriptions: Array<Promise<GraphQLResult<object>> | Observable<object>>;
    private mounted: boolean;

    constructor(props) {
        super(props);

        this.state = this.getInitialState();
        this.subSubscriptions = [];
    }

    getInitialState() {
        const { query } = this.props;
        return {
            loading: query && !!query.query,
            data: {},
            errors: [],
            mutation: () => console.warn('Not implemented'),
        };
    }

    getDefaultState() {
        return {
            loading: false,
            data: {},
            errors: [],
            mutation: () => console.warn('Not implemented'),
        };
    }

    async _fetchData() {
        this._unsubscribe();
        this.setState({ loading: true });

        const {
            // @ts-ignore
            query: { query, variables = {} } = {},
            // @ts-ignore
            mutation: { query: mutation, mutationVariables = {} } = {},
            subscription,
            onSubscriptionMsg = prevData => prevData,
        } = this.props;

        let { data, mutation: mutationProp, errors } = this.getDefaultState();

        if (
            !API ||
            typeof API.graphql !== 'function' ||
            typeof API.getGraphqlOperationType !== 'function'
        ) {
            throw new Error(
                'No API module found, please ensure @aws-amplify/api is imported'
            );
        }

        const hasValidQuery =
            query && API.getGraphqlOperationType(query) === 'query';
        const hasValidMutation =
            mutation && API.getGraphqlOperationType(mutation) === 'mutation';

        const validSubscription = subscription => subscription
            && subscription.query
            && API.getGraphqlOperationType(subscription.query) === 'subscription';

        const validateSubscriptions = (subscription) => {
            let valid = false;
            if(Array.isArray(subscription)) {
               valid = subscription.map(validSubscription).indexOf(false) === -1;
            } else {
               valid = validSubscription(subscription)
            }
            return valid;
        };

        const hasValidSubscriptions = validateSubscriptions(subscription);



        if (!hasValidQuery && !hasValidMutation && !hasValidSubscriptions) {
            console.warn('No query, mutation or subscription was specified correctly');
        }

        if (hasValidQuery) {
            console.log('hasValidQuery');
            try {
                data = null;

                const response = await API.graphql({ query, variables });

                // @ts-ignore
                data = response.data;
            } catch (err) {
                data = err.data;
                errors = err.errors;
            }
        }

        if (hasValidMutation) {
            console.log('hasValidMutation');
            // @ts-ignore
            mutationProp = async variables => {
                const result = await API.graphql({ query: mutation, variables });

                this.forceUpdate();
                return result;
            };
        }

        if (hasValidSubscriptions) {

            const connectSubscriptionToOnSubscriptionMessage = (subscription) => {

                const {query: subsQuery, variables: subsVars} = subscription;

                try {
                    const observable = API.graphql({
                        query: subsQuery,
                        variables: subsVars,
                    });

                    // @ts-ignore
                    this.subSubscriptions.push(observable.subscribe({
                        next: ({value: {data}}) => {
                            const {data: prevData} = this.state;
                            // @ts-ignore
                            const newData = onSubscriptionMsg(prevData, data);
                            if (this.mounted) {
                                this.setState({data: newData});
                            }
                        },
                        error: err => console.error(err),
                    }));

                } catch (err) {
                    errors = err.errors;
                }
            };
            if(Array.isArray(subscription)) {
                subscription.forEach(connectSubscriptionToOnSubscriptionMessage);
            } else {
                connectSubscriptionToOnSubscriptionMessage(subscription)
            }
        }

        this.setState({ data, errors, mutation: mutationProp, loading: false });
    }

    _unsubscribe() {
        const __unsubscribe = subSubscription => {
            if (subSubscription) {
                subSubscription.unsubscribe();
            }
        };
        this.subSubscriptions.map(__unsubscribe);
    }

    async componentDidMount() {
        console.log('Connect - componentDidMount');
        this._fetchData();
        this.mounted = true;
    }

    componentWillUnmount() {
        console.log('Connect - componentWillUnmount');
        this._unsubscribe();
        this.mounted = false;
    }

    componentDidUpdate(prevProps) {
        const { loading } = this.state;

        const { query: newQueryObj, mutation: newMutationObj, subscription: newSubscription} = this.props;
        const { query: prevQueryObj, mutation: prevMutationObj,  subscription: prevSubscription } = prevProps;

        // query
        // @ts-ignore
        const { query: newQuery, variables: newQueryVariables } = newQueryObj || {};
        // @ts-ignore
        const { query: prevQuery, variables: prevQueryVariables } =
        prevQueryObj || {};
        const queryChanged =
            prevQuery !== newQuery ||
            JSON.stringify(prevQueryVariables) !== JSON.stringify(newQueryVariables);

        // mutation
        // @ts-ignore
        const { query: newMutation, variables: newMutationVariables } =
        newMutationObj || {};
        // @ts-ignore
        const { query: prevMutation, variables: prevMutationVariables } =
        prevMutationObj || {};
        const mutationChanged =
            prevMutation !== newMutation ||
            JSON.stringify(prevMutationVariables) !==
            JSON.stringify(newMutationVariables);

        // subscription
        // @ts-ignore
        const { query: newSubsQuery, variables: newSubsVars } = newSubscription || {};
        // @ts-ignore
        const { query: prevSubsQuery, variables: prevSubsVars } = prevSubscription || {};
        const subscriptionChanged =
            prevSubsQuery !== newSubsQuery  ||
            JSON.stringify(prevSubsVars) !==
            JSON.stringify(newSubsVars);

        if (!loading && (queryChanged || mutationChanged || subscriptionChanged)) {
            this._fetchData();
        }
    }

    render() {
        const { data, loading, mutation, errors } = this.state;
        // @ts-ignore
        return this.props.children({ data, errors, loading, mutation }) || null;
    }
}

/**
 * @deprecated use named import
 */
export default Connect;

Usage (*example of onSubscriptionMessage is above)

<Connect
     query={graphqlOperation(listTopics)}
     subscription={[graphqlOperation(onCreateTopic),  graphqlOperation(onDeleteTopic)]}
     onSubscriptionMsg={onSubscriptionMessage}>
{.....}
</Connect>

from amplify-ui.

renschler avatar renschler commented on July 28, 2024

was the react Connect component deprecated?

from amplify-ui.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.