amplify-flutter
amplify-flutter copied to clipboard
Datastore not sync when add ownderField and/or groupsField in schema
Description
Using the schema below that has ownerField and groupsField both point to a list of string. When run the app in iOS, the datastore isn't sync'ed. In Android, the app crashed with error messages.
schema with two fields that cause issue
type Building2 @model @auth(rules: [{allow: owner, ownerField: "users"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
type Building3 @model @auth(rules: [{allow: owner}, {allow: groups, groupsField: "users"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
also tried this schema which has issue like above
type Building2 @model @auth(rules: [{allow: owner, ownerField: "users"}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
type Building3 @model @auth(rules: [{allow: owner}, {allow: groups, groupsField: "users"}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
schema without two fields that has no issue
type Building2 @model @auth(rules: [{allow: owner}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
type Building3 @model @auth(rules: [{allow: owner}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
Categories
- [ ] Analytics
- [ ] API (REST)
- [x] API (GraphQL)
- [ ] Auth
- [ ] Authenticator
- [x] DataStore
- [ ] Storage
Steps to Reproduce
- Create Flutter App with Amplify API schema with
ownerFieldandgroupsFieldboth point to a list of string (userIds) - Deploy the schema
- Run the app in iOS. In Terminal, it doesn't show the hub event
modelsyncedand no error messages. - Run the app in Android. In Terminal, it shows the hub event
modelsyncedwith error messages.
Launching lib/main.dart on sdk gphone64 arm64 in debug mode...
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
Connecting to VM Service at ws://127.0.0.1:63574/oRkmIQmibb4=/ws
[GETX] Instance "GetMaterialController" has been created
[GETX] Instance "GetMaterialController" has been initialized
[GETX] GOING TO ROUTE /login
[GETX] Instance "LoginController" has been created
[GETX] Instance "AuthService" has been created
[GETX] Instance "AmplifyService" has been created
[log] BEGIN INIT AMPLIFY
I/amplify:flutter:auth_cognito( 3631): Added Auth plugin
I/amplify:flutter:analytics_pinpoint( 3631): Added AnalyticsPinpoint plugin
I/amplify:flutter:api( 3631): Added API plugin
[GETX] Instance "AmplifyService" has been initialized
[GETX] Instance "AuthService" has been initialized
[GETX] Instance "LoginController" has been initialized
D/AWSMobileClient( 3631): Using the SignInProviderConfig from `awsconfiguration.json`.
W/AWSMobileClient( 3631): Failed to parse HostedUI settings from store. Defaulting to awsconfiguration.json
W/AWSMobileClient( 3631): java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
W/AWSMobileClient( 3631): at org.json.JSONTokener.nextCleanInternal(JSONTokener.java:121)
W/AWSMobileClient( 3631): at org.json.JSONTokener.nextValue(JSONTokener.java:98)
W/AWSMobileClient( 3631): at org.json.JSONObject.<init>(JSONObject.java:168)
W/AWSMobileClient( 3631): at org.json.JSONObject.<init>(JSONObject.java:185)
W/AWSMobileClient( 3631): at com.amazonaws.mobile.client.AWSMobileClient.getHostedUIJSON(AWSMobileClient.java:714)
W/AWSMobileClient( 3631): at com.amazonaws.mobile.client.AWSMobileClient$2.run(AWSMobileClient.java:615)
W/AWSMobileClient( 3631): at com.amazonaws.mobile.client.internal.InternalCallback$1.run(InternalCallback.java:101)
W/AWSMobileClient( 3631): at java.lang.Thread.run(Thread.java:920)
D/AWSMobileClient( 3631): initialize: Cognito HostedUI client detected
D/AWSMobileClient( 3631): Inspecting user state details
D/AutoSessionTracker( 3631): Activity paused: MainActivity
D/AutoSessionTracker( 3631): Activity created: com.amazonaws.amplify.amplify_analytics_pinpoint.EmptyActivity
I/amplify:aws-datastore( 3631): Creating table: Building3
I/amplify:aws-datastore( 3631): Creating table: PersistentRecord
I/amplify:aws-datastore( 3631): Creating table: Building2
I/amplify:aws-datastore( 3631): Creating table: PersistentModelVersion
I/amplify:aws-datastore( 3631): Creating table: LastSyncMetadata
I/amplify:aws-datastore( 3631): Creating table: ModelMetadata
I/amplify:aws-datastore( 3631): Creating index for table: PersistentRecord
D/AutoSessionTracker( 3631): Activity resumed: MainActivity
D/AutoSessionTracker( 3631): Application entered the foreground.
W/e.smarterblind( 3631): Accessing hidden method Landroid/app/AppOpsManager;->checkOpNoThrow(IILjava/lang/String;)I (unsupported, reflection, allowed)
W/e.smarterblind( 3631): Accessing hidden field Landroid/app/AppOpsManager;->OP_POST_NOTIFICATION:I (unsupported, reflection, allowed)
D/AutoSessionTracker( 3631): Activity destroyed com.amazonaws.amplify.amplify_analytics_pinpoint.EmptyActivity
[log] BEGIN INIT AMPLIFY HUB SCRIPTION
[log] END INIT AMPLIFY HUB SCRIPTION
[log] END INIT AMPLIFY
D/AWSMobileClient( 3631): Inspecting user state details
D/AWSMobileClient( 3631): waitForSignIn: userState:SIGNED_OUT
I/amplify:flutter:datastore( 3631): Unhandled DataStoreHubEvent: SUCCEEDED
I/amplify:flutter:datastore( 3631): com.amplifyframework.core.category.CategoryInitializationResult@5ee8743
[log] BEGIN AMPLIFY SIGNOUT
D/EGL_emulation( 3631): app_time_stats: avg=1796.79ms min=204.63ms max=4540.98ms count=3
W/AuthClient( 3631): HostedUIRedirectActivity is not declared in AndroidManifest.
D/IdentityManager( 3631): Signing out...
D/AWSMobileClient( 3631): Inspecting user state details
[log] END AMPLIFY SIGNOUT
[log] BEGIN CLEAR DATASTORE
I/amplify:aws-datastore( 3631): Orchestrator lock acquired.
I/amplify:aws-datastore( 3631): Orchestrator lock released.
I/amplify:aws-datastore( 3631): Creating table: Building3
I/amplify:aws-datastore( 3631): Creating table: PersistentRecord
I/amplify:aws-datastore( 3631): Creating table: Building2
I/amplify:aws-datastore( 3631): Creating table: PersistentModelVersion
I/amplify:aws-datastore( 3631): Creating table: LastSyncMetadata
I/amplify:aws-datastore( 3631): Creating table: ModelMetadata
I/amplify:aws-datastore( 3631): Creating index for table: PersistentRecord
I/amplify:flutter:datastore( 3631): Successfully cleared the store
[log] BEGIN CLEAR DATASTORE
[log] BEGIN LOGIN
W/CognitoUserSession( 3631): CognitoUserSession is not valid because idToken is null.
D/AWSMobileClient( 3631): Sending password.
D/AWSMobileClient( 3631): Using USER_SRP_AUTH for flow type.
D/AWSMobileClient( 3631): _federatedSignIn: Putting provider and token in store
D/AWSMobileClient( 3631): Inspecting user state details
D/AWSMobileClient( 3631): hasFederatedToken: false provider: cognito-idp.ca-central-1.amazonaws.com/ca-central-1_bZQUyxwy1
D/AWSMobileClient( 3631): Inspecting user state details
[log] hubEvent.eventName: SIGNED_IN
[log] SIGNED_IN
[log] @_amplifyService.status.listen: AmplifyStatus.signedIn
D/AWSMobileClient( 3631): hasFederatedToken: true provider: cognito-idp.ca-central-1.amazonaws.com/ca-central-1_bZQUyxwy1
[GETX] GOING TO ROUTE /home
[GETX] REMOVING ROUTE /login
D/AWSMobileClient( 3631): Inspecting user state details
D/AWSMobileClient( 3631): hasFederatedToken: true provider: cognito-idp.ca-central-1.amazonaws.com/ca-central-1_bZQUyxwy1
D/AWSMobileClient( 3631): waitForSignIn: userState:SIGNED_IN
D/AWSMobileClient( 3631): getCredentials: Validated user is signed-in
[GETX] Instance "HomeController" has been created
I/amplify:aws-datastore( 3631): Orchestrator lock acquired.
I/amplify:aws-datastore( 3631): Orchestrator transitioning from STOPPED to SYNC_VIA_API
I/amplify:aws-datastore( 3631): Starting to observe local storage changes.
[GETX] Instance "HomeController" has been initialized
I/amplify:aws-datastore( 3631): Now observing local storage. Local changes will be enqueued to mutation outbox.
I/amplify:aws-datastore( 3631): Setting currentState to LOCAL_ONLY
I/amplify:aws-datastore( 3631): Setting currentState to SYNC_VIA_API
I/amplify:flutter:datastore( 3631): Established a new stream form flutter com.amplifyframework.datastore.storage.sqlite.-$$Lambda$coxN3FV0myAqN-gpZfZvj7bzSOI@814523f
I/amplify:aws-datastore( 3631): Starting API synchronization mode.
I/amplify:aws-datastore( 3631): Starting processing subscription events.
I/amplify:aws-datastore( 3631): Orchestrator lock released.
W/amplify:aws-datastore( 3631): An error occurred on the remote ON_UPDATE subscription for model Building3
W/amplify:aws-datastore( 3631): java.lang.NullPointerException: Attempt to invoke interface method 'boolean java.util.List.isEmpty()' on a null object reference
W/amplify:aws-datastore( 3631): at com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator.isReadRestrictingStaticGroup(AuthRuleRequestDecorator.java:147)
W/amplify:aws-datastore( 3631): at com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator.decorate(AuthRuleRequestDecorator.java:103)
W/amplify:aws-datastore( 3631): at com.amplifyframework.api.aws.AWSApiPlugin.buildSubscriptionOperation(AWSApiPlugin.java:628)
W/amplify:aws-datastore( 3631): at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:308)
W/amplify:aws-datastore( 3631): at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:288)
W/amplify:aws-datastore( 3631): at com.amplifyframework.api.ApiCategory.subscribe(ApiCategory.java:91)
W/amplify:aws-datastore( 3631): at com.amplifyframework.datastore.appsync.AppSyncClient.subscription(AppSyncClient.java:332)
W/amplify:aws-datastore( 3631): at com.amplifyframework.datastore.appsync.AppSyncClient.onUpdate(AppSyncClient.java:272)
W/amplify:aws-datastore( 3631): at com.amplifyframework.datastore.syncengine.-$$Lambda$r7L8lscweM53-6nW0zECJRGgjT0.subscribe(Unknown Source:7)
W/amplify:aws-datastore( 3631): at com.amplifyframework.datastore.syncengine.SubscriptionProcessor.lambda$subscriptionObservable$6$SubscriptionProcessor(SubscriptionProcessor.java:187)
W/amplify:aws-datastore( 3631): at com.amplifyframework.datastore.syncengine.-$$Lambda$SubscriptionProcessor$w6tohapLGUGmW4mOmsvNOno7GVE.subscribe(Unknown Source:11)
...
E/AndroidRuntime( 3631): ... 8 more
D/AutoSessionTracker( 3631): Activity paused: MainActivity
I/Process ( 3631): Sending signal. PID: 3631 SIG: 9
Lost connection to device.
Exited (sigterm)
Screenshots
No response
Platforms
- [x] iOS
- [x] Android
Android Device/Emulator API Level
No response
Environment
[✓] Flutter (Channel stable, 2.10.5, on macOS 12.3.1 21E258 darwin-arm, locale
en-CA)
• Flutter version 2.10.5 at /Users/<user>/fvm/versions/2.10.5
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 5464c5bac7 (2 weeks ago), 2022-04-18 09:55:37 -0700
• Engine revision 57d3bac3dd
• Dart version 2.16.2
• DevTools version 2.9.2
[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
• Android SDK at /Users/<user>/Library/Android/sdk
• Platform android-31, build-tools 31.0.0
• Java binary at: /Applications/Android
Studio.app/Contents/jre/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 13.3.1)
• Xcode at /Applications/XCode.app/Contents/Developer
• CocoaPods version 1.11.3
[✓] Android Studio (version 2020.3)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)
[✓] VS Code (version 1.66.2)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.38.1
[✓] Connected device (3 available)
• sdk gphone64 arm64 (mobile) • emulator-5554 •
android-arm64 • Android 12 (API 31) (emulator)
• iPhone 13 Pro (mobile) • 1CA31312-509F-4F0C-9A9D-DABC5FE54518 • ios
• com.apple.CoreSimulator.SimRuntime.iOS-15-4 (simulator)
• iPhone 13 Pro Max (mobile) • 273BEB6B-5F10-4914-B1EB-15E19F906CD0 • ios
• com.apple.CoreSimulator.SimRuntime.iOS-15-4 (simulator)
[✓] HTTP Host Availability
• All required HTTP hosts are available
• No issues found!
Dependencies
Dart SDK 2.16.2
Flutter SDK 2.10.5
smarterblinds 1.0.0+1
dependencies:
- amplify_analytics_pinpoint 0.4.5 [amplify_analytics_plugin_interface amplify_analytics_pinpoint_android amplify_analytics_pinpoint_ios amplify_core flutter plugin_platform_interface]
- amplify_api 0.4.5 [amplify_api_plugin_interface amplify_core collection flutter meta plugin_platform_interface]
- amplify_auth_cognito 0.4.5 [flutter amplify_auth_plugin_interface amplify_core amplify_auth_cognito_android amplify_auth_cognito_ios collection plugin_platform_interface]
- amplify_datastore 0.4.5 [flutter amplify_datastore_plugin_interface amplify_core plugin_platform_interface meta collection async]
- amplify_flutter 0.4.5 [amplify_analytics_plugin_interface amplify_api_plugin_interface amplify_auth_plugin_interface amplify_core amplify_datastore_plugin_interface amplify_storage_plugin_interface collection flutter json_annotation meta plugin_platform_interface]
- cupertino_icons 1.0.4
- flutter 0.0.0 [characters collection material_color_utilities meta typed_data vector_math sky_engine]
- get 4.6.1 [flutter]
transitive dependencies:
- amplify_analytics_pinpoint_android 0.4.5 [flutter]
- amplify_analytics_pinpoint_ios 0.4.5 [flutter]
- amplify_analytics_plugin_interface 0.4.5 [amplify_core flutter meta]
- amplify_api_plugin_interface 0.4.5 [amplify_core collection flutter json_annotation meta]
- amplify_auth_cognito_android 0.4.5 [flutter]
- amplify_auth_cognito_ios 0.4.5 [amplify_core flutter]
- amplify_auth_plugin_interface 0.4.5 [flutter meta amplify_core]
- amplify_core 0.4.5 [flutter plugin_platform_interface collection date_time_format meta uuid]
- amplify_datastore_plugin_interface 0.4.5 [flutter meta collection amplify_core]
- amplify_storage_plugin_interface 0.4.5 [flutter meta amplify_core]
- async 2.8.2 [collection meta]
- characters 1.2.0
- collection 1.15.0
- crypto 3.0.2 [typed_data]
- date_time_format 2.0.1
- json_annotation 4.5.0 [meta]
- material_color_utilities 0.1.3
- meta 1.7.0
- plugin_platform_interface 2.1.2 [meta]
- sky_engine 0.0.99
- typed_data 1.3.0 [collection]
- uuid 3.0.6 [crypto]
- vector_math 2.1.1
Device
Android Emulator Pixel 3a
OS
Android 12.0 (API 31) arm64
CLI Version
8.1.0
Additional Context
No response
Hi @surisakc can you paste the generated Dart class files for Building2 and Building3?
Hi @surisakc can you paste the generated Dart class files for
Building2andBuilding3?
@HuiSF here they are
import 'ModelProvider.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
/** This is an auto generated class representing the Building2 type in your schema. */
@immutable
class Building2 extends Model {
static const classType = const _Building2ModelType();
final String id;
final String? _name;
final GenericStatus? _status;
final String? _data;
final List<String>? _users;
final TemporalDateTime? _createdAt;
final TemporalDateTime? _updatedAt;
@override
getInstanceType() => classType;
@override
String getId() {
return id;
}
String get name {
try {
return _name!;
} catch(e) {
throw new AmplifyCodeGenModelException(
AmplifyExceptionMessages.codeGenRequiredFieldForceCastExceptionMessage,
recoverySuggestion:
AmplifyExceptionMessages.codeGenRequiredFieldForceCastRecoverySuggestion,
underlyingException: e.toString()
);
}
}
GenericStatus get status {
try {
return _status!;
} catch(e) {
throw new AmplifyCodeGenModelException(
AmplifyExceptionMessages.codeGenRequiredFieldForceCastExceptionMessage,
recoverySuggestion:
AmplifyExceptionMessages.codeGenRequiredFieldForceCastRecoverySuggestion,
underlyingException: e.toString()
);
}
}
String? get data {
return _data;
}
List<String>? get users {
return _users;
}
TemporalDateTime? get createdAt {
return _createdAt;
}
TemporalDateTime? get updatedAt {
return _updatedAt;
}
const Building2._internal({required this.id, required name, required status, data, users, createdAt, updatedAt}): _name = name, _status = status, _data = data, _users = users, _createdAt = createdAt, _updatedAt = updatedAt;
factory Building2({String? id, required String name, required GenericStatus status, String? data, List<String>? users}) {
return Building2._internal(
id: id == null ? UUID.getUUID() : id,
name: name,
status: status,
data: data,
users: users != null ? List<String>.unmodifiable(users) : users);
}
bool equals(Object other) {
return this == other;
}
@override
bool operator ==(Object other) {
if (identical(other, this)) return true;
return other is Building2 &&
id == other.id &&
_name == other._name &&
_status == other._status &&
_data == other._data &&
DeepCollectionEquality().equals(_users, other._users);
}
@override
int get hashCode => toString().hashCode;
@override
String toString() {
var buffer = new StringBuffer();
buffer.write("Building2 {");
buffer.write("id=" + "$id" + ", ");
buffer.write("name=" + "$_name" + ", ");
buffer.write("status=" + (_status != null ? enumToString(_status)! : "null") + ", ");
buffer.write("data=" + "$_data" + ", ");
buffer.write("users=" + (_users != null ? _users!.toString() : "null") + ", ");
buffer.write("createdAt=" + (_createdAt != null ? _createdAt!.format() : "null") + ", ");
buffer.write("updatedAt=" + (_updatedAt != null ? _updatedAt!.format() : "null"));
buffer.write("}");
return buffer.toString();
}
Building2 copyWith({String? id, String? name, GenericStatus? status, String? data, List<String>? users}) {
return Building2._internal(
id: id ?? this.id,
name: name ?? this.name,
status: status ?? this.status,
data: data ?? this.data,
users: users ?? this.users);
}
Building2.fromJson(Map<String, dynamic> json)
: id = json['id'],
_name = json['name'],
_status = enumFromString<GenericStatus>(json['status'], GenericStatus.values),
_data = json['data'],
_users = json['users']?.cast<String>(),
_createdAt = json['createdAt'] != null ? TemporalDateTime.fromString(json['createdAt']) : null,
_updatedAt = json['updatedAt'] != null ? TemporalDateTime.fromString(json['updatedAt']) : null;
Map<String, dynamic> toJson() => {
'id': id, 'name': _name, 'status': enumToString(_status), 'data': _data, 'users': _users, 'createdAt': _createdAt?.format(), 'updatedAt': _updatedAt?.format()
};
static final QueryField ID = QueryField(fieldName: "building2.id");
static final QueryField NAME = QueryField(fieldName: "name");
static final QueryField STATUS = QueryField(fieldName: "status");
static final QueryField DATA = QueryField(fieldName: "data");
static final QueryField USERS = QueryField(fieldName: "users");
static var schema = Model.defineSchema(define: (ModelSchemaDefinition modelSchemaDefinition) {
modelSchemaDefinition.name = "Building2";
modelSchemaDefinition.pluralName = "Building2s";
modelSchemaDefinition.authRules = [
AuthRule(
authStrategy: AuthStrategy.OWNER,
ownerField: "users",
identityClaim: "cognito:username",
provider: AuthRuleProvider.USERPOOLS,
operations: [
ModelOperation.CREATE,
ModelOperation.UPDATE,
ModelOperation.DELETE,
ModelOperation.READ
])
];
modelSchemaDefinition.addField(ModelFieldDefinition.id());
modelSchemaDefinition.addField(ModelFieldDefinition.field(
key: Building2.NAME,
isRequired: true,
ofType: ModelFieldType(ModelFieldTypeEnum.string)
));
modelSchemaDefinition.addField(ModelFieldDefinition.field(
key: Building2.STATUS,
isRequired: true,
ofType: ModelFieldType(ModelFieldTypeEnum.enumeration)
));
modelSchemaDefinition.addField(ModelFieldDefinition.field(
key: Building2.DATA,
isRequired: false,
ofType: ModelFieldType(ModelFieldTypeEnum.string)
));
modelSchemaDefinition.addField(ModelFieldDefinition.field(
key: Building2.USERS,
isRequired: false,
isArray: true,
ofType: ModelFieldType(ModelFieldTypeEnum.collection, ofModelName: describeEnum(ModelFieldTypeEnum.string))
));
modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField(
fieldName: 'createdAt',
isRequired: false,
isReadOnly: true,
ofType: ModelFieldType(ModelFieldTypeEnum.dateTime)
));
modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField(
fieldName: 'updatedAt',
isRequired: false,
isReadOnly: true,
ofType: ModelFieldType(ModelFieldTypeEnum.dateTime)
));
});
}
class _Building2ModelType extends ModelType<Building2> {
const _Building2ModelType();
@override
Building2 fromJson(Map<String, dynamic> jsonData) {
return Building2.fromJson(jsonData);
}
}
import 'ModelProvider.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
/** This is an auto generated class representing the Building3 type in your schema. */
@immutable
class Building3 extends Model {
static const classType = const _Building3ModelType();
final String id;
final String? _name;
final GenericStatus? _status;
final String? _data;
final List<String>? _users;
final TemporalDateTime? _createdAt;
final TemporalDateTime? _updatedAt;
@override
getInstanceType() => classType;
@override
String getId() {
return id;
}
String get name {
try {
return _name!;
} catch(e) {
throw new AmplifyCodeGenModelException(
AmplifyExceptionMessages.codeGenRequiredFieldForceCastExceptionMessage,
recoverySuggestion:
AmplifyExceptionMessages.codeGenRequiredFieldForceCastRecoverySuggestion,
underlyingException: e.toString()
);
}
}
GenericStatus get status {
try {
return _status!;
} catch(e) {
throw new AmplifyCodeGenModelException(
AmplifyExceptionMessages.codeGenRequiredFieldForceCastExceptionMessage,
recoverySuggestion:
AmplifyExceptionMessages.codeGenRequiredFieldForceCastRecoverySuggestion,
underlyingException: e.toString()
);
}
}
String? get data {
return _data;
}
List<String>? get users {
return _users;
}
TemporalDateTime? get createdAt {
return _createdAt;
}
TemporalDateTime? get updatedAt {
return _updatedAt;
}
const Building3._internal({required this.id, required name, required status, data, users, createdAt, updatedAt}): _name = name, _status = status, _data = data, _users = users, _createdAt = createdAt, _updatedAt = updatedAt;
factory Building3({String? id, required String name, required GenericStatus status, String? data, List<String>? users}) {
return Building3._internal(
id: id == null ? UUID.getUUID() : id,
name: name,
status: status,
data: data,
users: users != null ? List<String>.unmodifiable(users) : users);
}
bool equals(Object other) {
return this == other;
}
@override
bool operator ==(Object other) {
if (identical(other, this)) return true;
return other is Building3 &&
id == other.id &&
_name == other._name &&
_status == other._status &&
_data == other._data &&
DeepCollectionEquality().equals(_users, other._users);
}
@override
int get hashCode => toString().hashCode;
@override
String toString() {
var buffer = new StringBuffer();
buffer.write("Building3 {");
buffer.write("id=" + "$id" + ", ");
buffer.write("name=" + "$_name" + ", ");
buffer.write("status=" + (_status != null ? enumToString(_status)! : "null") + ", ");
buffer.write("data=" + "$_data" + ", ");
buffer.write("users=" + (_users != null ? _users!.toString() : "null") + ", ");
buffer.write("createdAt=" + (_createdAt != null ? _createdAt!.format() : "null") + ", ");
buffer.write("updatedAt=" + (_updatedAt != null ? _updatedAt!.format() : "null"));
buffer.write("}");
return buffer.toString();
}
Building3 copyWith({String? id, String? name, GenericStatus? status, String? data, List<String>? users}) {
return Building3._internal(
id: id ?? this.id,
name: name ?? this.name,
status: status ?? this.status,
data: data ?? this.data,
users: users ?? this.users);
}
Building3.fromJson(Map<String, dynamic> json)
: id = json['id'],
_name = json['name'],
_status = enumFromString<GenericStatus>(json['status'], GenericStatus.values),
_data = json['data'],
_users = json['users']?.cast<String>(),
_createdAt = json['createdAt'] != null ? TemporalDateTime.fromString(json['createdAt']) : null,
_updatedAt = json['updatedAt'] != null ? TemporalDateTime.fromString(json['updatedAt']) : null;
Map<String, dynamic> toJson() => {
'id': id, 'name': _name, 'status': enumToString(_status), 'data': _data, 'users': _users, 'createdAt': _createdAt?.format(), 'updatedAt': _updatedAt?.format()
};
static final QueryField ID = QueryField(fieldName: "building3.id");
static final QueryField NAME = QueryField(fieldName: "name");
static final QueryField STATUS = QueryField(fieldName: "status");
static final QueryField DATA = QueryField(fieldName: "data");
static final QueryField USERS = QueryField(fieldName: "users");
static var schema = Model.defineSchema(define: (ModelSchemaDefinition modelSchemaDefinition) {
modelSchemaDefinition.name = "Building3";
modelSchemaDefinition.pluralName = "Building3s";
modelSchemaDefinition.authRules = [
AuthRule(
authStrategy: AuthStrategy.OWNER,
ownerField: "owner",
identityClaim: "cognito:username",
provider: AuthRuleProvider.USERPOOLS,
operations: [
ModelOperation.CREATE,
ModelOperation.UPDATE,
ModelOperation.DELETE,
ModelOperation.READ
]),
AuthRule(
authStrategy: AuthStrategy.GROUPS,
groupClaim: "cognito:groups",
groupsField: "groups",
provider: AuthRuleProvider.USERPOOLS,
operations: [
ModelOperation.CREATE,
ModelOperation.UPDATE,
ModelOperation.DELETE,
ModelOperation.READ
])
];
modelSchemaDefinition.addField(ModelFieldDefinition.id());
modelSchemaDefinition.addField(ModelFieldDefinition.field(
key: Building3.NAME,
isRequired: true,
ofType: ModelFieldType(ModelFieldTypeEnum.string)
));
modelSchemaDefinition.addField(ModelFieldDefinition.field(
key: Building3.STATUS,
isRequired: true,
ofType: ModelFieldType(ModelFieldTypeEnum.enumeration)
));
modelSchemaDefinition.addField(ModelFieldDefinition.field(
key: Building3.DATA,
isRequired: false,
ofType: ModelFieldType(ModelFieldTypeEnum.string)
));
modelSchemaDefinition.addField(ModelFieldDefinition.field(
key: Building3.USERS,
isRequired: false,
isArray: true,
ofType: ModelFieldType(ModelFieldTypeEnum.collection, ofModelName: describeEnum(ModelFieldTypeEnum.string))
));
modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField(
fieldName: 'createdAt',
isRequired: false,
isReadOnly: true,
ofType: ModelFieldType(ModelFieldTypeEnum.dateTime)
));
modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField(
fieldName: 'updatedAt',
isRequired: false,
isReadOnly: true,
ofType: ModelFieldType(ModelFieldTypeEnum.dateTime)
));
});
}
class _Building3ModelType extends ModelType<Building3> {
const _Building3ModelType();
@override
Building3 fromJson(Map<String, dynamic> jsonData) {
return Building3.fromJson(jsonData);
}
}
@HuiSF FYI, here is the configuration that works i.e. the datastore is sync'ed up at the app's startup and the datastore has the correct Building2 records when login with different accounts based on the ownerField group. This only works in iPhone. Android still has the same error.
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub::username", ownerField: "users"}, {allow: private, provider: iam}]) { id: ID! name: String! status: GenericStatus! data: String users: [String] }
Hi @surisakc based on what you've written, it seems like you saved these models before changing your schema.graphql?
If that is correct, could you try clearing your database, and deleting your app locally (to clear local store), rebuilding your schema.graphql and then saving/syncing your models again?
Hi @surisakc based on what you've written, it seems like you saved these models before changing your schema.graphql?
If that is correct, could you try clearing your database, and deleting your app locally (to clear local store), rebuilding your schema.graphql and then saving/syncing your models again?
Hi @fjnoyp, from what I did test in Android last Friday with the latest schema I mentioned above, if I deleted the datastore locally and ran the app, it would sync up the datastore with correct data for an account. If I logged-out and logged-in with different account, it didn't sync up the datastore with correct data for this account (still saw the dataset belonged to the previous account). Again, if I deleted the datastore after logged-out every time, then next login it will sync up datastore correctly.
Could it be the issue with @manyToMany used in model User and Family? Android doesn't delete/clear datastore file when logout and next login with different account it fails to sync? Here is the error got from the debugger log:
W/amplify:aws-datastore( 4279): Unauthorized failure:ON_DELETE FamilyUsers
enum GenericStatus {
ACTIVE
INACTIVE
DELETED
}
enum WindowStatus {
not_yet_measured
select_styles
order_pending
order_received
order_in_progress
order_in_transit
not_yet_installed
installed
pairing_pending
paired
deleted
}
enum FamilyRole {
OWNER
KID
USER
ADMIN
}
enum AddressType {
billing
shipping
}
type Address @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
line1: String!
line2: String
city: String!
state: String!
country: String!
postalCode: String!
type: AddressType
isBilling: Boolean
isShipping: Boolean
isSelected: Boolean
status: GenericStatus!
data: String
}
type ProductCategory @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
id: ID!
name: String!
verbose_name: String
notes: String
status: GenericStatus!
data: String
custom_fields: String
components: [Component] @hasMany(indexName: "byProductCategory", fields: ["id"])
productID: ID @index(name: "byProduct")
product: Product @belongsTo(fields: ["productID"])
genericproductID: ID @index(name: "byGenericProduct")
genericproduct: GenericProduct @belongsTo(fields: ["genericproductID"])
}
type Component @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
id: ID!
name: String!
verbose_name: String
notes: String
status: GenericStatus!
data: String
custom_fields: String
productcategoryID: ID @index(name: "byProductCategory", sortKeyFields: ["name"])
productcategory: ProductCategory @belongsTo(fields: ["productcategoryID"])
}
type Product @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
id: ID!
name: String!
verbose_name: String
description: String
product_line: String
external_id: String
price: Float
notes: String
status: GenericStatus!
data: String
custom_fields: String
manufacturerID: ID
manufacturer: Manufacturer @hasOne(fields: ["manufacturerID"])
componentID: ID
component: Component @hasOne(fields: ["componentID"])
categories: [ProductCategory] @hasMany(indexName: "byProduct", fields: ["id"])
genericproductID: ID @index(name: "byGenericProduct")
genericproduct: GenericProduct @belongsTo(fields: ["genericproductID"])
}
type Manufacturer @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
id: ID!
name: String
verbose_name: String
notes: String
status: GenericStatus!
data: String
custom_fields: String
}
type Partner @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
id: ID!
name: String
verbose_name: String
address: String
contact: String
notes: String
status: GenericStatus!
data: String
custom_fields: String
}
type GenericProduct @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
id: ID!
name: String!
verbose_name: String
configuration: String
notes: String
status: GenericStatus!
data: String
custom_fields: String
products: [Product] @hasMany(indexName: "byGenericProduct", fields: ["id"])
categories: [ProductCategory] @hasMany(indexName: "byGenericProduct", fields: ["id"])
}
type Window @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: WindowStatus!
data: String
families: [Family] @hasMany(indexName: "byWindowFamily", fields: ["id"])
users: [User] @hasMany(indexName: "byWindowUser", fields: ["id"])
roomID: ID
room: Room @hasOne(fields: ["roomID"])
}
type Room @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
buildingID: ID
building: Building @hasOne(fields: ["buildingID"])
families: [Family] @hasMany(indexName: "byRoomFamily", fields: ["id"])
users: [User] @hasMany(indexName: "byRoomUser", fields: ["id"])
}
type Building @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
families: [Family] @hasMany(indexName: "byBuildingFamily", fields: ["id"])
users: [User] @hasMany(indexName: "byBuildingUser", fields: ["id"])
}
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub::username", ownerField: "users"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
#type Building3 @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: groups, groupsField: "users"}, {allow: private, provider: iam}]) {
# id: ID!
# name: String!
# status: GenericStatus!
# data: String
# users: [String]
#}
# @manyToMany creates a relationship table FamilyUsers
type Family @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
members: [User] @manyToMany(relationName: "FamilyUsers")
windowID: ID @index(name: "byWindowFamily", sortKeyFields: ["name"])
window: Window @belongsTo(fields: ["windowID"])
roomID: ID @index(name: "byRoomFamily", sortKeyFields: ["name"])
room: Room @belongsTo(fields: ["roomID"])
buildingID: ID @index(name: "byBuildingFamily", sortKeyFields: ["name"])
building: Building @belongsTo(fields: ["buildingID"])
}
type User @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
id: ID!
email: String!
status: GenericStatus!
data: String
families: [Family] @manyToMany(relationName: "FamilyUsers")
windowID: ID @index(name: "byWindowUser", sortKeyFields: ["email"])
window: Window @belongsTo(fields: ["windowID"])
roomID: ID @index(name: "byRoomUser", sortKeyFields: ["email"])
room: Room @belongsTo(fields: ["roomID"])
buildingID: ID @index(name: "byBuildingUser", sortKeyFields: ["email"])
building: Building @belongsTo(fields: ["buildingID"])
}
type Client @model @auth(rules: [{ allow: owner, identityClaim: "sub::username" }, { allow: private, provider: iam }]) {
id: ID!
firstname: String!
lastname: String!
email: String!
telephone: String!
claimed: Boolean
data: String
}
Hello @surisakc sorry for the delayed response.
I did some digging, looking at your schema: ownerField: "users", you are trying to use the dynamic group authorization.
type Building2 @model @auth(rules: [{allow: owner, ownerField: "users"}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
There is a known limitation with AppSync, that realtime subscription is not supported with dynamic group authorization. Hence Amplify DataStore doesn't support this type of authorization as well, but only static group authorization. Please see the documentation.
Hi @HuiSF, thank you for your response.
I saw in the document about the known limitation as well but from my test the iOS worked if I set {allow: owner, identityClaim: "sub::username", ownerField: "users"}. With that I thought Android may also work but it didn't. Android will work if the datastore file gets deleted when sign out but from my test, it failed to delete the datastore file with the error that it couldn't clear the relation table FamilyUser (which is not defined explicitly in the schema as it is created on the fly if not wrong from two models Family and User when using @manyToMany). When I saw such error message pointing to the relation table not being deleted at sign out, I thought that was the issue that prevented the datastore to be synced next sign in (as when I manually deleted the datastore file and next sign in it synced up correct data) and if it could delete the datastore at sign out then next sign in in Android it would sync up the correct datastore for correct account. When I saw this behavior, I wasn't sure if it related to the know limitation (which it should mean it shouldn't sync up datastore at all in any platform; iOS nor Android).
We need this dynamic group authorization in our app. If we use the static group authorization, it means the app has to dynamically create and delete a group and add users (userIds) in the group if I understand correctly and I may have to use the AWS Cognito API library directly if Amplify doesn't provide the API to do so, correct?
Thanks!
Hi @surisakc , for this auth rule {allow: owner, identityClaim: "sub::username", ownerField: "users"} (owner field is a list), as the document states, it also doesn't support AppSync realtime subscription, I'd expect feature gaps in platform libraries, which cause exceptions. :/
Your thought is correct using low level Cognito API to create groups. But I'm not sure if it's a good idea, as Cognito quota has a limit on creating user groups.
Do you mind to described your use case design around the groups? I'll see if we can find any alternatives to resolve this.
@HuiSF I agreed with you on using the low level Cognito API to create groups at runtime may not be a good solution. Using the groups as static should be ideal.
From what I was testing, we wanted to share some models in datastore which means;
- User A creates a model/data/record and saves it in his datastore (he is the owner).
- User A invites User B to see the model/data later by adding User B ID into a group in this case
users: [String]andownerField: "users". If using low level Cognito API to create a runtime group, saygroupA, and add User B ID to the CognitogroupA, later on it could be more users to be added and also we have to delete groups if not used anymore (and also the Cognito quota limits). - User B logins and should see the data created by User A in his datastore.
- User B could modify the data and User A could see the changes in realtime.
- Note that we want to take advantage of offline feature in Amplify if we could. We knew that if we used Amplify GraphQL API which is online feature, the syncing part may work better but that's not what planned.
@HuiSF A quick question about using a lambda function to override authorization rule in schema.
- Is it possible if I set the owner field to a family ID (to share a model in a family with many users) using the lambda function while the logged-in user can see that model/data in his datastore? Will it break the concept of the datastore update and sync?
Hi @surisakc
Is it possible if I set the owner field to a family ID (to share a model in a family with many users) using the lambda function while the logged-in user can see that model/data in his datastore? Will it break the concept of the datastore update and sync?
I doubt this will work as when you rely on owner based auth, it will check db owner field value and the username attached int he JWT token. For this case I think it breaks.
But have you tried something like this?
- Add a custom attribute to userpool, let's say
familyId. - Take this schema
- set
ownerFieldasfamilyId - override
identityClaimto use the custom attribute
- set
# quick example, I haven't verified this is working
type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", identityClaim: "familyId"}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
familyId: String
}
- When adding a user to a family, assign
familyId(probably need a lambda function or API to do this separately outside of Amplify) - When saving model in datastore, you should be able to get
familyIdby getting user attribute, and assign its value to the modelfamilyIdfield, which will be used as "owner" - When you sync, mutate model, the auth will compare against
familyIdto enforce permission
Add a custom attribute to userpool, let's say familyId.
Hi @HuiSF Thanks for the reply. For the solution you suggested. I have some questions.
- From the schema you provided, I set the
ownerFieldtofamilyIdas your example in some models that they can be shared among users in the same family. - In Cognito Users Pool, I add a new custom field
familyId. - In the app, when
UserAlogged-in and wants to share a modelBuilding2for instance toUserB and UserC, I need to, say, use AWS Cognito API calledAdminUpdateUsername()or something similar to add the custom fieldfamilyIdand assign the family ID value and update the account ofUserA(may be able to update from the app for the logged-in user) and to update to the other accounts ofUserBandUserC, I may need to implement a function in the backend (REST) or a lambda function which will call the same Cognito API function to add and set thefamilyIdto those accounts. - When
UserAdoesn't want to share the model anymore that the Cognito API can set null or empty value to the custom field in those accounts, correct?
Here is how I tested it and if it was correct steps then it didn't work. Let me know if I missed any step here.
- I did try updating the schema (one at a time) with different
identifyClaimas below.
type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", identityClaim: **"familyId"**}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
familyId: String
}
type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", identityClaim: **"custom:familyId"**}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
familyId: String
}
type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", identityClaim: **"custom::familyId"**}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
familyId: String
}
-
I added a custom attribute
custom:familyIdin Cognito Users Pool and update the attribute to my account ascustom:familyId = "familyA"which showed like below when viewed my account in Cognito console.
-
I then went to Amplify Studio and updated the field
familyIdwith the same value "familyA" I used in the custom attribute above.
-
I logged-in with my Cognito account (with correct
custom:familyIdset) but the datastore didn't sync up with correctBuilding2data i.e. the table was empty.
Thanks!
The process you described makes sense to me.
When UserA doesn't want to share the model anymore that the Cognito API can set null or empty value to the custom field in those accounts, correct?
Yes I believe so, setting null will remove the auth token to retrieve records for that particular user with owner based auth.
With this use case, I think you need to poll userAttributes of currently signed in user often (or other equivalent), to ensure the user is still assigned with that particular familyID, if the familyID is changed (becomes null or a different) you may need to invoke DataStore.clear() then restart DataStore to re-sync Data.
With this use case, I think you need to poll userAttributes of currently signed in user often (or other equivalent), to ensure the user is still assigned with that particular
familyID, if thefamilyIDis changed (becomesnullor a different) you may need to invokeDataStore.clear()then restart DataStore to re-sync Data.
But from testing, this solution doesn't work (the datastore doesn't have the Building2 data).
I saw something similar to your suggestion from here https://docs.amplify.aws/cli/graphql/authorization-rules/#configure-custom-identity-and-group-claims which I did try like schema below but it didn't work either. Does it have to be {allow: owner, identityClaim: "familyId"} or {allow: owner, identityClaim: "custom:familyId"}? Do you have an example that works?
Example I:
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "familyId"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
owner: String
}
Example II:
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "custom:familyId"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
owner: String
}
I set Building2.owner: "abdf51d5-cd60-45c1-8970-55d0972cb7ec" and my Cognito Account custom:familyId: "abdf51d5-cd60-45c1-8970-55d0972cb7ec". I also tried to set both with string familyA. It doesn't work at all.
By setting {allow: owner, identityClaim: "custom:familyId"}, is it by default using the accessToken for access authorization or will it use idtoken which has identity claim in it (I saw my idtoken had the claims with correct custom:family_id with correct value)? I did check in the resolvers and it should get the identity claim for access authorization. Do I have to do anything like update Authorization Header somewhere some how for this to work?
#if( $util.authType() == "User Pool Authorization" )
#if( !$isAuthorized )
#set( $ownerEntity0 = $util.defaultIfNull($ctx.result.familyId, null) )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("custom:family_id"), "___xamznone____") )
#set( $ownerAllowedFields0 = ["id","name","status","data","familyId","_version","_deleted","_lastChangedAt"] )
#set( $ownerNullAllowedFields0 = ["id","name","status","data","familyId"] )
#set( $isAuthorizedOnAllFields0 = true )
#if( $ownerEntity0 == $ownerClaim0 )
#if( $isAuthorizedOnAllFields0 )
#set( $isAuthorized = true )
#else
$util.qr($allowedFields.addAll($ownerAllowedFields0))
$util.qr($nullAllowedFields.addAll($ownerNullAllowedFields0))
#end
#end
#end
#end
Hi @surisakc looking at the resolver, it listed fields in the model schema as
["id","name","status","data","familyId","_version","_deleted","_lastChangedAt"]
Which indicates your model should have familyId field instead of owner... Does this resolver match your model schema?
By setting {allow: owner, identityClaim: "custom:familyId"}, is it by default using the accessToken for access authorization or will it use idtoken which has identity claim in it (I saw my idtoken had the claims with correct custom:family_id with correct value)?
I think it uses access token by default, might need to set idToken to the HTTP request Authorization header, though amplify-flutter doesn't support custom header yet. And using id token may not be the best practice for security...
Which indicates your model should have
familyIdfield instead ofowner... Does this resolver match your model schema?
Hi @HuiSF, sorry for not posting the latest schema which has familyId.
type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", provider: userPools, identityClaim: "custom:family_id"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
familyId: String
#owner: String
}
I think it uses access token by default, might need to set idToken to the HTTP request Authorization header, though amplify-flutter doesn't support custom header yet. And using id token may not be the best practice for security...
Hi @HuiSF Thank you for your response. So, for the example that using identityClaim: "user_id" in this https://docs.amplify.aws/cli/graphql/authorization-rules/#configure-custom-identity-and-group-claims, does it mean I have to add a custom attribute in Cognito called user_id correct? And Cognito will create it to be custom:user_id, correct? If so, from the example in the link above, how is it going to work for the sync part if Amplify uses the access token but it doesn't contain the custom attribute? My point is if I didn't understand how to setup identityClaim from the link or if there is a working way that I missed?
Another question is when the app starts up first time, how does Amplify sync datastore? i.e. which AppSync API function that Amplify will use to sync datastore at this stage and which resolver.
i.e.
AppSync API: syncBuilding2s()
Resolver: Query.syncBuilding2s.auth.1.req.vlt
Thanks!
Hello @surisakc
Sorry for the delay -
So, for the example that using identityClaim: "user_id" in this https://docs.amplify.aws/cli/graphql/authorization-rules/#configure-custom-identity-and-group-claims, does it mean I have to add a custom attribute in Cognito called user_id correct? And Cognito will create it to be custom:user_id, correct?
This example is for using 3rd-party auth provider (OIDC in the doc) with which you override the identity claim to use the fields listed in the JWT token generated by this 3rd-party, not an attribute managed by Cognito.
Based on the findings and your testing, I think using custom user attributes along with idToken unfortunately is not doable now due to
- amplify-flutter API plugin doesn't support setting custom headers at this moment
- DataStore plugin doesn't set custom headers when making mutation/query requests to AppSync via the API plugin
Another question is when the app starts up first time, how does Amplify sync datastore?
It making requests using all the sync queries (one sync query per model). Were you looking at the generated lambda function files? You should be able to get a better view in AppSync console ("functions " in left navigation menu), where you can view the functions and their implementations associated with sync queries. e.g.
Additional thoughts:
In the doc you linked in the latest comment, it mentioned about "Pre Token Generation Lambda Trigger", it has potentials to modify the token before the service return the token to client. This example shows a way to override user group.
Were you looking at the generated lambda function files?
Hi @HuiSF I was guessing and checking the generated lambda functions in AppSync console which generated from the resolver templates generated by the amplify CLI on my machine under the folder amplify/backend, correct?
- Has anyone requested the same business logic as I did i.e. to be able to share models/data among authorized users?
- Is there a complete example on how to create a custom lambda authorizer in NodeJS? i.e. advanced technique how to get the value from a field in a model
Building2.familyIdfor instance to compare to the Cognito custom attributecustom:familyIdand set theisAuthorizedto false or true and if it's true, what data to return in the lambda, see the example below. - Will the custom lambda authorizer like below work with DataStore syncing when the app starts and with DataStore functions like
Amplify.Datastore.save/update/deleteor it only works withAmplify.API.save/update/delete? - Does the custom lambda authorizer work with schema
@auth(rules: [{allow: custom}])forDatastoreor it's forGraphQL APIonly?
/**
* @type {import('@types/aws-lambda').APIGatewayProxyHandler}
*/
exports.handler = async (event) => {
console.log(`EVENT: ${JSON.stringify(event)}`);
const {
authorizationToken,
requestContext: { apiId, accountId },
} = event;
const response = {
isAuthorized: authorizationToken === 'custom-authorized',
resolverContext: {
userid: 'user-id',
info: 'contextual information A',
more_info: 'contextual information B',
},
deniedFields: [
`arn:aws:appsync:${process.env.AWS_REGION}:${accountId}:apis/${apiId}/types/Event/fields/comments`,
`Mutation.createEvent`,
],
ttlOverride: 300,
};
console.log(`response >`, JSON.stringify(response, null, 2));
return response;
};
under the folder amplify/backend, correct?
That's correct.
Has anyone requested the same business logic as I did i.e. to be able to share models/data among authorized users?
Not I'm aware of, but this is like a multi-tenants use case there may be similar requests in AppSync but I don't have a list in handy.
Is there a complete example on how to create a custom lambda authorizer in NodeJS?
Need to search around.
Will the custom lambda authorizer like below work with DataStore syncing when the app starts and with DataStore functions like Amplify.Datastore.save/update/delete or it only works with Amplify.API.save/update/delete?
If API plugin APIs work with this custom authorizer, including subscription, then DataStore should work with it too, as DataStore is fully depends on API plugin.
Does the custom lambda authorizer work with schema @auth(rules: [{allow: custom}]) for Datastore or it's for GraphQL API only?
It should work for DataStore as well as the answer to the previous question.
By setting {allow: owner, identityClaim: "custom:familyId"}, is it by default using the accessToken for access authorization or will it use idtoken which has identity claim in it (I saw my idtoken had the claims with correct custom:family_id with correct value)?
Regarding this point, I'm syncing up with amplify-ios, amplify-android and amplify-cli maintainers to determine the correct behavior - whether the amplify libraries should resolve the custom claim from idToken when nothing can't be resolved from access token.
I'm syncing up with amplify-ios, amplify-android and amplify-cli maintainers to determine the correct behavior - whether the amplify libraries should resolve the custom claim from idToken when nothing can't be resolved from access token.
Hi @HuiSF Thank you for checking! I did more testing and it seems if amplify-android library for flutter could somehow makes the library to work like amplify-ios (for flutter) then it may fix the issue i.e. if amplify-android can support Test 3 like amplify-ios does.
Test 2 and Test 4 partially work in Android because the identityClaim is in the accessToken. While Test 3 the identityClaim isn't in the accessToken and it doesn't work in Android but somehow it works in iOS.
Here are my tests
Flutter: v2.10.5 Amplify Flutter: v0.5.1 Amplify CLI: 8.3.1
Schema using a list of owners which has known limitation (not support realtime subscription)
- Note although the document said using the schema with this auth rule won't have a realtime subscription but from testing it works in iOS but not Android. This is the closest schema that I thought by fixing something (may be in Android?) and it may work (not sure if there will be any other issues).
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub::username", ownerField: "users"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
-
Test 1:
{allow: owner, identityClaim: "cognito:username", ownerField: "users"}(the claim isn’t in accessToken but in idToken)- doesn’t work in iOS.
- doesn’t work in Android.
-
Test 2:
{allow: owner, identityClaim: "username", ownerField: "users"}(the claim is in accessToken)- doesn’t work in iOS. only see
networkstatusamplify event, notmodelsynced - works in Android with error below and the datastore sync works sometimes (with or without the error), not every time. (same as ``{allow: owner, identityClaim: "sub", ownerField: "users"}` )
- doesn’t work in iOS. only see
GraphQLResponseException{message=Subscription error for Building2: [GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onDeleteBuilding2'', locations='null', path='null', extensions='null'}], errors=[GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onDeleteBuilding2'', locations='null', path='null', extensions='null'}], recoverySuggestion=See attached list of GraphQLResponse.Error objects.}
- Test 3:
{allow: owner, identityClaim: "sub::username", ownerField: "users"}(the claim isn’t in accessToken)- works in iOS.
- doesn’t work in Android, got error
I/amplify:aws-datastore( 3827): Setting currentState to STOPPED
I/amplify:flutter:datastore( 3827): Successfully stopped datastore remote synchronization
I/amplify:aws-datastore( 3827): Orchestrator lock released.
E/amplify:aws-datastore( 3827): Failure encountered while attempting to start API sync.
E/amplify:aws-datastore( 3827): DataStoreException{message=Error during subscription., cause=ApiAuthException{message=Attempted to subscribe to a model with owner-based authorization without sub::username which was specified (or defaulted to) as the identity claim., cause=null, recoverySuggestion=If you did not specify a custom identityClaim in your schema, make sure you are logged in. If you did, check that the value you specified in your schema is present in the access key.}, recoverySuggestion=Evaluate details.}
E/amplify:aws-datastore( 3827): at com.amplifyframework.datastore.appsync.AppSyncClient.lambda$subscription$3(AppSyncClient.java:331)
E/amplify:aws-datastore( 3827): at com.amplifyframework.datastore.appsync.-$$Lambda$AppSyncClient$797ziDK0io-qXODzROLOA77stS8.accept(Unknown Source:4)
W/amplify:aws-datastore( 3827): DataStoreException{message=Error during subscription., cause=ApiAuthException{message=Attempted to subscribe to a model with owner-based authorization without sub::username which was specified (or defaulted to) as the identity claim., cause=null, recoverySuggestion=If you did not specify a custom identityClaim in your schema, make sure you are logged in. If you did, check that the value you specified in your schema is present in the access key.}, recoverySuggestion=Evaluate details.}
W/amplify:aws-datastore( 3827): at com.amplifyframework.datastore.appsync.AppSyncClient.lambda$subscription$3(AppSyncClient.java:331)
W/amplify:aws-datastore( 3827): at com.amplifyframework.datastore.appsync.-$$Lambda$AppSyncClient$797ziDK0io-qXODzROLOA77stS8.accept(Unknown Source:4)
- Test 4:
{allow: owner, identityClaim: "sub", ownerField: "users"}(the claim is in accessToken)- doesn’t work in iOS.
- work in Android with error below and the datastore sync works sometimes (with or without the error), not every time. Also the generated resolver
Subscription.onDeleteBuilding2.auth.1.req.vtl(see the Original Resolver below) doesn't have the part#if( $util.authType() == "User Pool Authorization" )(wondering if this part means it is the known limitation that doesn't support the realtime subscription?). I did create a custom resolver to override the missing part (see the Custom Resolver below) thinking it may fix the issue but it doesn't.
Original Resolver
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
#set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
#foreach( $adminRole in $adminRoles )
#if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
#return($util.toJson({}))
#end
#end
#if( !$isAuthorized )
#if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
#set( $isAuthorized = true )
#end
#end
#end
#if( $util.authType() == "User Pool Authorization" )
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **
Custom Resolver
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
#set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
#foreach( $adminRole in $adminRoles )
#if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
#return($util.toJson({}))
#end
#end
#if( !$isAuthorized )
#if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
#set( $isAuthorized = true )
#end
#end
#end
#if( $util.authType() == "User Pool Authorization" )
#if( !$isAuthorized )
#set( $ownerEntity0 = $util.defaultIfNull($ctx.result.users, null) )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
#set( $ownerClaimsList0 = [] )
#foreach( $allowedOwner in $ownerEntity0 )
#if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
#set( $isAuthorized = true )
#end
#end
#end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **
Test 4 Error
GraphQLResponseException{message=Subscription error for Building2: [GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onDeleteBuilding2'', locations='null', path='null', extensions='null'}], errors=[GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onDeleteBuilding2'', locations='null', path='null', extensions='null'}], recoverySuggestion=See attached list of GraphQLResponse.Error objects.}
z
- Test 5:
{allow: owner, identityClaim: "custom:familyId", ownerField: "familyId"}(the claim is in idToken which not support in Datastore, accessToken is defaulted)- doesn’t work in iOS.
- doesn’t work in Android.
Hi @HuiSF
I think this solution works in both iOS and Android to share models among users. I didn't test by removing the three custom Subscription resolvers as I thought they may be needed but may be not.
Schema
- Use the attribute existing in
accessToken(in this case usesub)
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub", ownerField: "users"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
3 Custom Subscription Resolvers project/amplify/backend/api/<api name>/resolvers
FILE: Subscription.onCreateBuilding2.auth.1.req
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
#set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
#foreach( $adminRole in $adminRoles )
#if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
#return($util.toJson({}))
#end
#end
#if( !$isAuthorized )
#if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
#set( $isAuthorized = true )
#end
#end
#end
#if( $util.authType() == "User Pool Authorization" )
#if( !$isAuthorized )
#set( $ownerEntity0 = $util.defaultIfNull($ctx.args.input.users, null) )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
#set( $ownerClaimsList0 = [] )
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____")))
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____"))))
#set( $ownerAllowedFields0 = ["id","name","status","data","users","_version","_deleted","_lastChangedAt"] )
#set( $isAuthorizedOnAllFields0 = true )
#foreach( $allowedOwner in $ownerEntity0 )
#if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
#if( $isAuthorizedOnAllFields0 )
#set( $isAuthorized = true )
#break
#else
$util.qr($allowedFields.addAll($ownerAllowedFields0))
#end
#end
#end
#if( $util.isNull($ownerEntity0) && !$ctx.args.input.containsKey("users") )
$util.qr($ctx.args.input.put("users", [$ownerClaim0]))
#if( $isAuthorizedOnAllFields0 )
#set( $isAuthorized = true )
#else
$util.qr($allowedFields.addAll($ownerAllowedFields0))
#end
#end
#end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **
=====================================================================
FILE: Subscription.onDeleteBuilding2.auth.1.req
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
#set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
#foreach( $adminRole in $adminRoles )
#if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
#return($util.toJson({}))
#end
#end
#if( !$isAuthorized )
#if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
#set( $isAuthorized = true )
#end
#end
#end
#if( $util.authType() == "User Pool Authorization" )
#if( !$isAuthorized )
#set( $ownerEntity0 = $util.defaultIfNull($ctx.result.users, null) )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
#set( $ownerClaimsList0 = [] )
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____")))
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____"))))
#foreach( $allowedOwner in $ownerEntity0 )
#if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
#set( $isAuthorized = true )
#end
#end
#end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **
=====================================================================
FILE: Subscription.onUpdateBuilding2.auth.1.req
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
#set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
#foreach( $adminRole in $adminRoles )
#if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
#return($util.toJson({}))
#end
#end
#if( !$isAuthorized )
#if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
#set( $isAuthorized = true )
#end
#end
#end
#if( $util.authType() == "User Pool Authorization" )
#if( !$isAuthorized )
#set( $ownerEntity0 = $util.defaultIfNull($ctx.result.users, []) )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
#set( $ownerClaimsList0 = [] )
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____")))
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____"))))
#set( $ownerAllowedFields0 = ["id","name","status","data","users","_version","_deleted","_lastChangedAt"] )
#set( $ownerNullAllowedFields0 = ["id","name","status","data","users"] )
#set( $isAuthorizedOnAllFields0 = true )
#foreach( $allowedOwner in $ownerEntity0 )
#if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
#if( $isAuthorizedOnAllFields0 )
#set( $isAuthorized = true )
#break
#else
$util.qr($allowedFields.addAll($ownerAllowedFields0))
$util.qr($nullAllowedFields.addAll($ownerNullAllowedFields0))
#end
#end
#end
#end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **
Update the Generated Models
identityClaim: "sub"only works in Android when sync but not in iOS.identityClaim: "sub::username"only works in iOS when sync but not in
// identityClaim: "sub",
identityClaim: GetPlatform.isAndroid ? "sub" : "sub::username",
Error in Android
- From testing in Android, the datastore was sync'ed correctly when login with different accounts. Not sure if this will cause any issues later on or not.
W/amplify:aws-datastore(17573): API sync failed - transitioning to LOCAL_ONLY.
W/amplify:aws-datastore(17573): GraphQLResponseException{message=Subscription error for Building2: [GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onUpdateBuilding2'', locations='null', path='null', extensions='null'}], errors=[GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onUpdateBuilding2'', locations='null', path='null', extensions='null'}], recoverySuggestion=See attached list of GraphQLResponse.Error objects.}
W/amplify:aws-datastore(17573): at com.amplifyframework.datastore.appsync.AppSyncClient.lambda$subscription$2(AppSyncClient.java:324)
I think this solution works in both iOS and Android to share models among users. I didn't test by removing the three custom Subscription resolvers as I thought they may be needed but may be not.
For Android, it doesn't work every time when start the app and login i.e. the datastore isn't sync'ed every time (the Amplify event modelSynced isn't fired sometimes).
Validation error of type UnknownArgument: Unknown field argument users @ 'onUpdateBuilding2'
This error causes subscription to fail, which stops sync engine so you would see the unsuccessful sync. This error is due to the subscription selection set contains field is not defined in GraphQL schema (located in <projectRoot>/amplify/backend/<your-api-name>/build/schema.graphql), it should not be related to auth in particular.
This error causes subscription to fail, which stops sync engine so you would see the unsuccessful sync. This error is due to the subscription selection set contains field is not defined in GraphQL schema (located in
<projectRoot>/amplify/backend/<your-api-name>/build/schema.graphql), it should not be related to auth in particular.
Here is my schema. The error points to the unknown field argument users but the field is in the schema. I also made sure the 3 subscription resolvers had the field users in them. The error seems to be from this module AppSyncClient.java. Is there anything else in schema I should set?
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub", ownerField: "users"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}
Here is one of the three subscription resolvers.
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
#set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
#foreach( $adminRole in $adminRoles )
#if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
#return($util.toJson({}))
#end
#end
#if( !$isAuthorized )
#if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
#set( $isAuthorized = true )
#end
#end
#end
#if( $util.authType() == "User Pool Authorization" )
#if( !$isAuthorized )
#set( $ownerEntity0 = $util.defaultIfNull($ctx.result.users, []) )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
#set( $ownerClaimsList0 = [] )
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____")))
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____"))))
#set( $ownerAllowedFields0 = ["id","name","status","data","users","_version","_deleted","_lastChangedAt"] )
#set( $ownerNullAllowedFields0 = ["id","name","status","data","users"] )
#set( $isAuthorizedOnAllFields0 = true )
#foreach( $allowedOwner in $ownerEntity0 )
#if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
#if( $isAuthorizedOnAllFields0 )
#set( $isAuthorized = true )
#break
#else
$util.qr($allowedFields.addAll($ownerAllowedFields0))
$util.qr($nullAllowedFields.addAll($ownerNullAllowedFields0))
#end
#end
#end
#end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **
This error causes subscription to fail, which stops sync engine so you would see the unsuccessful sync. This error is due to the subscription selection set contains field is not defined in GraphQL schema (located in
<projectRoot>/amplify/backend/<your-api-name>/build/schema.graphql), it should not be related to auth in particular.
Hi @HuiSF I have fixed this issue but not sure if this is the right way and if it will have any other issues later on. Basically I have to go to AppSync Console and search in Schema for those Subscription functions that set in the schema to use ownerField="users" and users: [String] and the generated Subscription functions didn't have an input parameter (users: String) as expected in the error. I updated them to be like below. Now both iOS and Android can sync datastore.
Original
type Subscription {
onCreateBuilding2: Building2
@aws_subscribe(mutations: ["createBuilding2"])
@aws_iam
@aws_cognito_user_pools
onUpdateBuilding2: Building2
@aws_subscribe(mutations: ["updateBuilding2"])
@aws_iam
@aws_cognito_user_pools
onDeleteBuilding2: Building2
@aws_subscribe(mutations: ["deleteBuilding2"])
@aws_iam
@aws_cognito_user_pools
}
The fix that works
type Subscription {
onCreateBuilding2(users: String): Building2
@aws_subscribe(mutations: ["createBuilding2"])
@aws_iam
@aws_cognito_user_pools
onUpdateBuilding2(users: String): Building2
@aws_subscribe(mutations: ["updateBuilding2"])
@aws_iam
@aws_cognito_user_pools
onDeleteBuilding2(users: String): Building2
@aws_subscribe(mutations: ["deleteBuilding2"])
@aws_iam
@aws_cognito_user_pools
}
There is other issue in the Datastore library (Amplify_Flutter v0.5.1) of iOS and Android that doesn't work with the complex query anymore (let me know if you want me to open an issue for this)
Original Datastore query works in both iOS and Android in Amplify_Flutter v0.2.10
var _components =
await this.retrieveData<Component>(modelName: 'Component', queryParams: Component.NAME.eq('width').and(Component.STATUS.eq(GenericStatus.ACTIVE)));
To make the query to work in iOS, I have to separate the where clause into two steps:
var _list = await this.retrieveData<Component>(modelName: 'Component', queryParams: Component.NAME.eq('width'));
if (_list != null) {
var _components = _list.where((element) => element.status == GenericStatus.ACTIVE).toList();
}
To make the query to work in Android, I have to update the two query fields (see My fields) to be explicit naming like below, otherwise it will have an error about the ambiguous field. If I update the generated fields directly to be static final QueryField NAME = QueryField(fieldName: "component.name"), this will cause the error when syncing datastore:
class Component extends Model {
...
// My fields
static final QueryField NAME$ = QueryField(fieldName: "component.name");
static final QueryField STATUS$ = QueryField(fieldName: "component.status");
// Generated fields
static final QueryField ID = QueryField(fieldName: "component.id");
static final QueryField NAME = QueryField(fieldName: "name");
static final QueryField VERBOSE_NAME = QueryField(fieldName: "verbose_name");
static final QueryField NOTES = QueryField(fieldName: "notes");
static final QueryField STATUS = QueryField(fieldName: "status");
...
}
var _components =
await this.retrieveData<Component>(modelName: 'Component', queryParams: Component.NAME$.eq('width').and(Component.STATUS$.eq(GenericStatus.ACTIVE)));
Hi @surisakc Thanks for providing the details about the workaround.
I have fixed this issue but not sure if this is the right way and if it will have any other issues later on
So far, I'm unsure if Amplify CLI and GraphQL transformer do have the support of your use case... But looking at your schema and resolver changes it makes sense to me. As long as your custom resolvers are able to ensure correct auth scope for your data, I think it's fine to use it.
I will find some time to do some testing with your workaround as well.
There is other issue in the Datastore library (Amplify_Flutter v0.5.1) of iOS and Android that doesn't work with the complex query anymore (let me know if you want me to open an issue for this)
We do have integration tests for ensure the compound query to be working. And I just ran the tests based on version 0.5.1 and I didn't see any issues.
Could you sharing your implementation of retrieveData?