aws-amplify / amplify-flutter

A declarative library with an easy-to-use interface for building Flutter applications on AWS.
https://docs.amplify.aws
Apache License 2.0
1.31k stars 243 forks source link

Not all datastore hub events have corresponding DataStoreHubEvent classes and OutboxMutationFailed no longer exists #3452

Open dgagnon opened 1 year ago

dgagnon commented 1 year ago

Description

When building switch cases to handle all possible DataStoreHubEvent, many of the underlying events do not have corresponding DataStoreHubEvent classes:

see amplify_datastore-1.2.0-supports-only-mobile+1\lib\src\amplify_datastore_stream_controller.dart:

This forces developper to instead check for the eventName string. This not ideal for 2 reasons:

Also, Amplify-Android has an event called OutboxMutationFailed, which is very useful is it allows reacting right away when an error occurs. I see that swift does not seem to have an equivalent. I would be great to have this in amplify-flutter.
See https://docs.amplify.aws/lib/datastore/datastore-events/q/platform/android/#outboxmutationfailed

Categories

Steps to Reproduce

  1. create a demo app
  2. Add auth and datastore categories
  3. use code from the doc

Screenshots

  /// It is important to configure the HubChannel before configuring Amplify
  Future<DatastoreService> _configureDatastoreService() async {
    /// Avoid piling on subscriptions
    if (_isDatastoreConfigured == false) {
      /// This is a critical process and the user UI is not built yet
      /// no controllers exist yet to catch this error, so we catch it here
      try {
        hubDatastoreSubscription = Amplify.Hub.listen(HubChannel.DataStore, (hubEvent) {
          dbg("datastore hub event received: ${hubEvent.eventName.toString()}", cat: 'DATASTORE');
          switch (hubEvent.eventName) {
            /// We received a ready event, emitting [readyState]
            case "ready":
              isDatastoreReady = true;
              readyState.enter();

            /// We received a ready event, emitting [readyState]
            case "outboxMutationFailed":
              // isDatastoreReady = true;
              err("Outbox Mutation Error", null, cat: 'DATA');
              outboxMutationFailedState.enter();

            /// We received a networkStatus event, emitting [networkState] or [noNetworkState]
            case "networkStatus":
              final status = hubEvent.payload as NetworkStatusEvent?;
              isNetworkAvailable.value = status?.active ?? false;
              isNetworkAvailable.value ? networkState.enter() : noNetworkState.enter();

            /// We received a outboxStatus event, emitting [outboxEmptyState] or [outboxNotEmptyState]
            case "outboxStatus":
              final status = hubEvent.payload as OutboxStatusEvent?;
              isOutboxEmpty.value = status?.isEmpty ?? true;
              isOutboxEmpty.value ? outboxEmptyState.enter() : outboxNotEmptyState.enter();

            /// We received a subscriptionsEstablished event, emitting [subscriptionsEstablishedState]
            case "subscriptionsEstablished":
              subscriptionsEstablishedState.enter();

            /// We received a syncQueriesStartedState event, emitting [syncQueriesStartedState]
            case "syncQueriesStarted":
              syncQueriesStartedState.enter();

            /// We received a modelSynced event, emitting [modelSyncedStartedState]
            case "modelSynced":
              modelSyncedStartedState.enter();

            /// We received a syncQueriesReady event, emitting [syncQueriesReadyState]
            case "syncQueriesReady":
              syncQueriesReadyState.enter();
            case "subscriptionDataProcessed":
              break;
            default:
              err("Unknown DatastoreEvent received: ${hubEvent.eventName}", null, cat: 'DATASTORE');
          }
        });
      } catch (e, stackTrace) {
        err('Could not setup DatastoreService subscription. We can not continue. $e', stackTrace, cat: 'DATASTORE');
        if (kDebugMode) rethrow;
      }
    }
    return this;
  }

Platforms

Flutter Version

3.10.5

Amplify Flutter Version

1.2.0-supports-only-mobile+1

Deployment Method

Amplify CLI

Schema

enum Status {
    ACTIVE
    COMPLETED
    DELETED
}

enum UserStatus {
    ACTIVE
    SUSPENDED
    DELETED
}

type User @model @auth(rules: [{ allow: owner}, {allow: private, provider: iam}]) {
    id: ID! @auth(rules: [{ allow: owner }, {allow: private, provider: iam}])
    username: String! @auth(rules: [{ allow: owner }, {allow: private, provider: iam}])
    data: AWSJSON @auth(rules: [{ allow: owner }, {allow: private, provider: iam}])
    status: UserStatus @default(value: "ACTIVE" ) @index(name: "byStatus") @auth(rules: [{ allow: owner }, {allow: private, provider: iam}])
    owner: String @auth(rules: [{ allow: owner, operations: [read, create, delete] }, { allow: private, provider: iam }])
}
type Project @model @auth(rules: [{ allow: owner}, {allow: private, operations: [read]}, {allow: private, provider: iam}, { allow: groups, groups: ["admin"] }]) {
    id: ID! @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
    title: String! @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
    description: String @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
    data: AWSJSON @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
    status: Status @default(value: "ACTIVE" ) @index(name: "byStatus") @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
    dueAt: AWSDateTime! @auth(rules: [{ allow: owner}, { allow: private, provider: iam }, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
    owner: String @index(name: "byOwner") @auth(rules: [{ allow: owner, operations: [read, create, delete] }, { allow: private, provider: iam }, {allow: private, operations: [read]},{ allow: groups, groups: ["admin"] }])
    tasks: [Task] @hasMany(indexName: "byProject", fields: ["id"]) #https://docs.amplify.aws/cli/graphql/data-modeling/#belongs-to-relationship
}
type Task @model @auth(rules: [{ allow: owner}, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }]) {
    id: ID @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }])
    title: String! @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }])
    description: String @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }])
    data: AWSJSON @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }])
    status: Status @default(value: "ACTIVE" ) @index(name: "byStatus") @auth(rules: [{ allow: owner }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"] }])
    dueAt: AWSDateTime! @auth(rules: [{ allow: owner }, { allow: private, provider: iam }, { allow: groups, groups: ["admin"], operations: [read] }])
    #    owners: [String]
    # BUG: https://github.com/aws-amplify/amplify-flutter/issues/1566
    owner: String @index(name: "byOwner") @auth(rules: [{ allow: owner, operations: [read, create, delete] }, { allow: private, provider: iam }, { allow: groups, groups: ["admin"], operations: [read] }])
    projectID: ID! @index(name: "byProject") #https://docs.amplify.aws/cli/graphql/data-modeling/#belongs-to-relationship
    project: Project! @belongsTo(fields: ["projectID"])
}
dgagnon commented 1 year ago

There does not seem to be an easy way to know which exceptions can be returned by datastore or the hub. Let me know if I should open another issue.

Equartey commented 1 year ago

Hey @dgagnon

Thank you for the detailed write up!

I agree this would be a significant improvement.

I have opened #3454 to address this gap.

Re: OutboxMutationFailed missing on Swift, I will follow up with the Swift team and provide an update when I have it.

Equartey commented 1 year ago

Opened a feature request on amplify-swift for the missing type.

Further discussion for amplify-swift will be tracked there.

Thanks again for bringing this to our attention!

dgagnon commented 1 year ago

I see this was fixed in 1.3.0, great work!
With the new enum in place, would probably be a good idea to update the doc as well; https://docs.amplify.aws/lib/datastore/datastore-events/q/platform/flutter/#usage

Do you guys take external PRs ?

If not:

    try {
      hubDatastoreSubscription = Amplify.Hub.listen(HubChannel.DataStore, (hubEvent) {
        switch (hubEvent.type) {
          case DataStoreHubEventType.ready:
            safePrint('Datastore is ready.');
          case DataStoreHubEventType.outboxMutationFailed:
            // BUG: Android only: https://github.com/aws-amplify/amplify-swift/issues/3102
            safePrint('Datastore outboxMutationFailed.');
          case DataStoreHubEventType.networkStatus:
            safePrint('Datastore networkStatus event.');
          case DataStoreHubEventType.outboxStatus:
            safePrint('Datastore outboxStatus event.');
          case DataStoreHubEventType.subscriptionsEstablished:
            safePrint('Datastore subscriptionsEstablished.');
          case DataStoreHubEventType.syncQueriesStarted:
            safePrint('Datastore syncQueriesStarted.');
          case DataStoreHubEventType.modelSynced:
            safePrint('Datastore modelSynced.');
          case DataStoreHubEventType.syncQueriesReady:
            safePrint('Datastore syncQueriesReady.');
          case DataStoreHubEventType.subscriptionDataProcessed:
            safePrint('Datastore subscriptionDataProcessed.');
          case DataStoreHubEventType.outboxMutationEnqueued:
            safePrint('Datastore outboxMutationEnqueued.');
          case DataStoreHubEventType.outboxMutationProcessed:
            safePrint('Datastore outboxMutationProcessed.');
        }
      });
    } catch (e) {
      safePrint('Could not setup the Datastore subscription: $e');
    }
dnys1 commented 1 year ago

@dgagnon Absolutely, external PRs are always appreciated! 🙏