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.33k stars 248 forks source link

Can not set a nullable string index to null #4475

Open osehmathias opened 8 months ago

osehmathias commented 8 months ago

Description

I have schema like this

type Entry @model {
  id: ID!
  draftRecordID: ID @index
  }

With Amplify GraphQL, I can set draftRecordID to null.

With Amplify DataStore, I can not, even with entry.copyWithModelFieldValues(draftRecordID: const ModelFieldValue.value(null));

Categories

Steps to Reproduce

Create schema with a nullable String index. Set the String index field to a value. Attempt to remove that value by setting it to null.

Screenshots

No response

Platforms

Flutter Version

3.19.1

Amplify Flutter Version

1.6.1

Deployment Method

Amplify CLI

Schema

type Entry {
  id: ID!
  draftRecordID: ID @index
  }
NikaHsn commented 8 months ago

Sorry that you are facing this issue and thanks for reporting it. We will look into this and get back to you when we have updates.

osehmathias commented 8 months ago

@NikaHsn - thanks, although I think it is a DataStore issue, not a GraphQL issue, as I experience this error with DataStore but not GraphQL.

Equartey commented 8 months ago

Hi @osehmathias, I am unable to reproduce this.

.copyWithModelFieldValues() returns a new instance of the model which needs to be saved. This example works for me.

final entry = Entry(draftRecordID: 'foo');
print('Before: ${entry.draftRecordID}'); // Before: foo
final newEntry = entry.copyWithModelFieldValues(
    draftRecordID: const ModelFieldValue.value(null),
);
print('After: ${newEntry.draftRecordID}'); // After: null

Can you confirm how you are calling the copy method?


Also I noticed your schema Entry should have the @model decorator. I don't think this is causing issues, but it may have other side effects later on.

type Entry @model {
  id: ID!
  draftRecordID: ID @index
}
osehmathias commented 8 months ago

Hi @Equartey

I am calling the copy method like so:

final entry = Entry(draftRecordID: 'foo');
print('Before: ${entry.draftRecordID}'); // Before: foo
final newEntry = entry.copyWithModelFieldValues(
    draftRecordID: const ModelFieldValue.value(null),
);
print('After: ${newEntry.draftRecordID}'); // After: null
await Amplify.DataStore.save(newEntry);

// now fetch .... 

Entry fetchedEntry = await Amplify.DataStore.query(Entry.classType, where xxxx)
print('Fetched: ${fetchedEntry.draftRecordID}'); // Fetched entry, not cleared

Also I noticed your schema Entry should have the @model decorator. I don't think this is causing issues, but it may have other side effects later on.

Thank you. It does have the @model decorator. I edited it down when I posted the issue.

Equartey commented 8 months ago

Hi @osehmathias

My apologies, I cannot reproduce this. I successfully nullified the property when saving before and after.

I tested on both iOS and Android with the same Flutter and Amplify versions. Here is my function & schema:

  Future<void> copyTest() async {
    final entry = Entry(draftRecordID: 'foo');
    print('Before: ${entry.draftRecordID}'); // Before: foo

    await Amplify.DataStore.save(entry);

    final resBefore = await Amplify.DataStore.query(Entry.classType,
        where: Entry.ID.eq(entry.id));
    print('resBefore: $resBefore'); // resBefore: Entry.draftRecordID = baz

    final newEntry = entry.copyWithModelFieldValues(
      draftRecordID: const ModelFieldValue.value(null),
    );
    print('After: ${newEntry.draftRecordID}'); // After: null

    await Amplify.DataStore.save(newEntry);

    final resAfter = await Amplify.DataStore.query(Entry.classType,
        where: Entry.ID.eq(entry.id));
    print('resAfter: ${resAfter}'); // resAfter: Entry.draftRecordID = null
  }
type Entry @model {
  id: ID!
  draftRecordID: ID @index
}

I'm not sure what could be the issue or different about our environments.

Can you confirm the ID's of the entries match?

Is there anything else specific with your setup that would help me reproduce the issue?

osehmathias commented 8 months ago

Hi @Equartey - I can see in the xcode logs that it sets it to null

"draftRecordID": Amplify.JSONValue.null,

However, when I refetch, the value is still there.

I have been able to get around this by setting the value of draftRecordID to '-' instead of null.

Equartey commented 8 months ago

Hi @osehmathias. I looked into this again and could not reproduce. Your workaround is reasonable, but understandably not ideal.

Is the same behavior observed on Android?

Are you able to reproduce this on a fresh project?

osehmathias commented 8 months ago

Sorry about the delay, @Equartey.

This is how it's used.

      Entry updatedEntry = currentEntry.copyWithModelFieldValues(
          draftRecordID: const ModelFieldValue.value(null));
      await ref.read(entriesProvider.notifier).updateEntry(updatedEntry);

In this pattern, the provider updates the DB.

// provider.dart

final entriesProvider =
    StateNotifierProvider<EntriesProvider, List<Entry>?>((ref) {
  return EntriesProvider(ref);
});

class EntriesProvider extends StateNotifier<List<Entry>?> {
  Ref ref;
  EntriesProvider(this.ref) : super(null) {
    fetchEntries();
  }

  Future<void> updateEntry(Entry updatedEntry) async {
    try {
      await Amplify.DataStore.save(updatedEntry);
    } catch (e) {
      if (kDebugMode) {
        print('Error saving GoalEntry to DataStore: $e');
      }
      return;
    }
    if (state != null) {
      final index = state!.indexWhere((entry) => entry.id == updatedEntry.id);
      if (index != -1) {
        state = [
          for (int i = 0; i < state!.length; i++)
            if (i == index) updatedEntry else state![i]
        ];
      }
    }
  }
  }

For this reason, the state updates immediately and the mutation appears to be successful.

However, if I query the object afresh, such as when the app lifecycle changes, which in turn fetches data from the DataStore and updates the state ...

// main.dart

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.resumed) {
      var _ = ref.refresh(refreshAllProvider);
      initAuthController();
    }
  }

Or simply by making a call to Amplify.DataStore.query, or by querying on the AppSync API, or by looking in DynamoDB, I can see that the field has not been set to null and remains with its old value.

However, if I set the value to a string, it will update.

Equartey commented 6 months ago

Hi @osehmathias, apologies for the delay.

I was able to reproduce this only on iOS, but not on Android. Are you seeing this issue on Android too?

Still digging into what the cause could be. We'll update you when we have more information to share.

osehmathias commented 6 months ago

Hi @Equartey

Thank you for the response. I have not tried to replicate this on Android yet, but as it happens, I am just about to develop the same app for Android so I will let you know once I have tested it and have that information.

Equartey commented 6 months ago

@osehmathias, understood. Keep up posted. Thanks.

filipesbragio commented 6 months ago

We seem to be having the same issue, except not on a basic type such as a string so the work around wouldn't work for us. It only happens with iOS, we haven't been able to reproduce it on android.

I suppose changing it to a list with only one value and then removing and re-adding to the list would solve it but I would very much like to avoid that.

Equartey commented 6 months ago

@filipesbragio thanks for the extra data point.

It appears iOS is dropping properties set to a null literal when making the request to AppSync. The work around would be to pass a non-null value to represent your type (if possible). If your property is a list, an empty list would also nullify the property. Same with an empty string.

We're currently investigating how to best resolve this and will update you as we can.