Code Monkey home page Code Monkey logo

Comments (10)

Jordan-Nelson avatar Jordan-Nelson commented on August 30, 2024 1

@MarlonJD Thanks for the clarification. We will track this as a feature request.

from amplify-flutter.

hangoocn avatar hangoocn commented on August 30, 2024 1

I have the same issue and want to sort the values by createdAt.
imo, if we define the secondary index like this:
.secondaryIndexes(index => [index('xxxId').sortKeys(['createdAt']).queryField('listByDate')])
the cli will generate an api like ModelQueries.listByDate({sortDirection: 'DESC'}) which is same as the amplify js behavior

from amplify-flutter.

Jordan-Nelson avatar Jordan-Nelson commented on August 30, 2024

Hello @MarlonJD - Thanks for taking the time to open the issue. I want to make sure I understand the proposal. It sounds like you have been able to achieve the desired behavior with custom GraphQL queries. Your proposal is for this to be supported out of the box without the use of customer queries. Is that correct?

from amplify-flutter.

MarlonJD avatar MarlonJD commented on August 30, 2024

Hello @Jordan-Nelson. Yes custom GraphQL is possible and it's easier with copyWith after #4365. I want to use secondary index query without custom query and variable otherwise it will waste of time. It's all there we can easily create this document automatically,

Without this proposal, it can be query with these codes:

ie:

final request = ModelQueries.list(
  Comment.classType,
  // where: TaskComment.TASK.eq(taskId), <--- same field filter shouldn't be there
);

const modifiedDoc = r"""query taskCommentsByDate($filter: ModelTaskCommentFilterInput, $limit: Int, $nextToken: String, @queryField: ID!) {
  taskCommentsByDate(filter: $filter, limit: $limit, nextToken: $nextToken, sortDirection: DESC, taskCommentsId: $queryField) {
    items {
      id
      content
      post {
        id
        content
        ...
      }
      taskCommentsId
      createdAt
      updatedAt
    }
    nextToken
  }""";

// ignore: prefer_final_locals, omit_local_variable_types
var modifiedVar = request.variables;

modifiedVar["queryField"] = taskId;

final response = await Amplify.API
        .query(
          request: request.copyWith(
            document: modifiedDocument,
            variables: modifiedVar,
            decodePath: "taskCommentsByDate",
          ),
        )
        .response;

Instead of these modifications, we can just type:

ModelQueries.listByIndex(
  TaskComment.classType,
  queryField: taskId, <--- this will be required field
  sortDirection: DESC, <--- this will be optional, default is DESC
  // where: TaskComment.TASK.eq(taskId), <-- where shouldn't be same filter, but it can be other filters
);

It will save a lot of time.

from amplify-flutter.

MarlonJD avatar MarlonJD commented on August 30, 2024

@Jordan-Nelson I created #4945 PR, for this issue.

from amplify-flutter.

MarlonJD avatar MarlonJD commented on August 30, 2024

@Equartey Hey there 👋. I raised PR for this proposal, I hope you can review.

from amplify-flutter.

Equartey avatar Equartey commented on August 30, 2024

Hi @MarlonJD, as usual, thank you for opening the issue and the accompanying PR.

First, we will need to investigate how to best address this for all our customers before we move forward with your proposal.

This process takes time, but we will provide updates here as we have them. Thanks again.

from amplify-flutter.

MarlonJD avatar MarlonJD commented on August 30, 2024

Hi @Equartey. Thank you very much for your reply. I'll be waiting for updates.

from amplify-flutter.

MarlonJD avatar MarlonJD commented on August 30, 2024

Hey @hangoocn, I already created PR for this and waiting to merge, if you don't want to wait you can create CustomModelQueries class and use this, you can copy from the PR #4945.

Here is the example of how you can use this:

Create folder and create 3 new files called model_queries.dart, model_queries_factory.dart and graphql_request_factory:

model_queries.dart:

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import "package:amplify_core/amplify_core.dart";
import "model_queries_factory.dart";

/// Static helpers to generate query `GraphQLRequest` instances from models generated by Amplify codegen.

class CustomModelQueries {
  CustomModelQueries._(); // only static methods here, prevent calling constructor
  /// Generates a request for a list of model instances from
  /// secondary index. The `modelType` must have a secondary index defined.
  /// Check out the Amplify documentation for more information on how to define
  /// secondary indexes. https://docs.amplify.aws/gen1/react/build-a-backend/graphqlapi/best-practice/query-with-sorting/
  ///
  /// ```dart
  /// final request = ModelQueries.listByIndex(Todo.classType, queryField: parentId);
  /// ```
  /// or optional parameters:
  /// ```dart
  /// final request = ModelQueries.listByIndex(
  ///   Todo.classType,
  ///   queryField: parentId,
  ///   sortDirection: "DESC", // ASC or DESC, default is DESC
  ///   indexName: "todosByDate", // default is the first secondary index
  ///   overrideQueryFieldType: "ID!", // override the query field type, default is "ID!", e.g. "String!"
  ///   where: Todo.TASK.eq(taskId), // If your query parameter is taskId, you cannot use in where at the same time
  /// );
  /// ```

  static GraphQLRequest<PaginatedResult<T>> listByIndex<T extends Model>(
    ModelType<T> modelType, {
    int? limit,
    String? queryField,
    String? sortDirection,
    String? indexName,
    String? overrideQueryFieldType,
    String? customQueryName,
    QueryPredicate? where,
    String? apiName,
    APIAuthorizationType? authorizationMode,
    Map<String, String>? headers,
  }) {
    return ModelQueriesFactory.instance.listByIndex<T>(
      modelType,
      limit: limit,
      where: where,
      apiName: apiName,
      authorizationMode: authorizationMode,
      headers: headers,
      queryField: queryField,
      sortDirection: sortDirection ?? "DESC",
      indexName: indexName,
      overrideQueryFieldType: overrideQueryFieldType,
      customQueryName: customQueryName,
    );
  }
}

// TODO(ragingsquirrel3): remove when https://github.com/dart-lang/sdk/issues/50748 addressed
// ignore_for_file: flutter_style_todos

model_queries_factory.dart:

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// ignore_for_file: public_member_api_docs

import "package:amplify_core/amplify_core.dart";
import "graphql_request_factory.dart";

class ModelQueriesFactory {
  // Singleton methods/properties
  // usage: ModelQueriesFactory.instance;
  ModelQueriesFactory._();

  static final ModelQueriesFactory _instance = ModelQueriesFactory._();

  static ModelQueriesFactory get instance => _instance;

  GraphQLRequest<T> get<T extends Model>(
    ModelType<T> modelType,
    ModelIdentifier<T> modelIdentifier, {
    String? apiName,
    APIAuthorizationType? authorizationMode,
    Map<String, String>? headers,
  }) {
    final variables = modelIdentifier.serializeAsMap();
    return GraphQLRequestFactory.instance.buildRequest<T>(
      modelType: modelType,
      modelIdentifier: modelIdentifier,
      variables: variables,
      requestType: GraphQLRequestType.query,
      requestOperation: GraphQLRequestOperation.get,
      apiName: apiName,
      authorizationMode: authorizationMode,
      headers: headers,
    );
  }

  GraphQLRequest<PaginatedResult<T>> list<T extends Model>(
    ModelType<T> modelType, {
    int? limit,
    QueryPredicate? where,
    String? apiName,
    APIAuthorizationType? authorizationMode,
    Map<String, String>? headers,
  }) {
    final filter = GraphQLRequestFactory.instance
        .queryPredicateToGraphQLFilter(where, modelType);
    final variables = GraphQLRequestFactory.instance
        .buildVariablesForListRequest(limit: limit, filter: filter);

    return GraphQLRequestFactory.instance.buildRequest<PaginatedResult<T>>(
      modelType: PaginatedModelType(modelType),
      variables: variables,
      requestType: GraphQLRequestType.query,
      requestOperation: GraphQLRequestOperation.list,
      apiName: apiName,
      authorizationMode: authorizationMode,
      headers: headers,
    );
  }

  GraphQLRequest<PaginatedResult<T>> listByIndex<T extends Model>(
    ModelType<T> modelType, {
    int? limit,
    QueryPredicate? where,
    String? apiName,
    APIAuthorizationType? authorizationMode,
    Map<String, String>? headers,
    String? queryField,
    String? sortDirection,
    String? indexName,
    String? overrideQueryFieldType,
    String? customQueryName,
  }) {
    final filter = GraphQLRequestFactory.instance
        .queryPredicateToGraphQLFilter(where, modelType);
    final variables = GraphQLRequestFactory.instance
        .buildVariablesForListRequest(limit: limit, filter: filter);

    return GraphQLRequestFactory.instance
        .buildRequestForSecondaryIndex<PaginatedResult<T>>(
      modelType: PaginatedModelType(modelType),
      variables: variables,
      requestType: GraphQLRequestType.query,
      requestOperation: "listWithIndex",
      apiName: apiName,
      authorizationMode: authorizationMode,
      headers: headers,
      queryField: queryField,
      sortDirection: sortDirection,
      indexName: indexName,
      overrideQueryFieldType: overrideQueryFieldType,
      customQueryName: customQueryName,
    );
  }
}

and graphql_request_factory.dart:

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import "package:amplify_api_dart/src/graphql/utils.dart";
import "package:amplify_core/amplify_core.dart";
import "package:collection/collection.dart";

// ignore_for_file: public_member_api_docs, deprecated_member_use

/// `"id"`, the name of the id field when a primary key not specified in schema
/// with `@primaryKey` annotation.
const _defaultIdFieldName = "id";
// other string constants
const _commaSpace = ", ";
const _openParen = "(";
const _closeParen = ")";

/// Contains expressions for mapping request variables to expected places in the
/// request.
///
/// Some examples include ids or indexes for simple queries, filters, and input
/// variables/conditions for mutations.
class DocumentInputs {
  const DocumentInputs(this.upper, this.lower);

  // Upper document input: ($id: ID!)
  final String upper;
  // Lower document input: (id: $id)
  final String lower;
}

class GraphQLRequestFactory {
  GraphQLRequestFactory._();

  static final GraphQLRequestFactory _instance = GraphQLRequestFactory._();

  static GraphQLRequestFactory get instance => _instance;

  String _getName(ModelSchema schema, GraphQLRequestOperation operation) {
    // schema has been validated & schema.pluralName is non-nullable
    return operation == GraphQLRequestOperation.list
        ? schema.pluralName!
        : schema.name;
  }

  String _getSelectionSetFromModelSchema(
    ModelSchema schema,
    GraphQLRequestOperation operation, {
    bool ignoreParents = false,
  }) {
    // Schema has been validated & schema.fields is non-nullable.
    // Get a list of field names to include in the request body.
    final fields = schema.fields!.entries
        .where(
      (entry) => entry.value.association == null,
    ) // ignore related model fields
        .map((entry) {
      if (entry.value.type.fieldType == ModelFieldTypeEnum.embedded ||
          entry.value.type.fieldType == ModelFieldTypeEnum.embeddedCollection) {
        final embeddedSchema =
            getModelSchemaByModelName(entry.value.type.ofCustomTypeName!, null);
        final embeddedSelectionSet = _getSelectionSetFromModelSchema(
          embeddedSchema,
          GraphQLRequestOperation.get,
        );
        return "${entry.key} { $embeddedSelectionSet }";
      }
      return entry.key;
    }).toList(); // e.g. ["id", "name", "createdAt"]

    // If belongsTo, also add selection set of parent.
    final allBelongsTo = getBelongsToFieldsFromModelSchema(schema);
    for (final belongsTo in allBelongsTo) {
      final belongsToModelName = belongsTo.type.ofModelName;
      if (belongsToModelName != null && !ignoreParents) {
        final parentSchema =
            getModelSchemaByModelName(belongsToModelName, null);
        final parentSelectionSet = _getSelectionSetFromModelSchema(
          parentSchema,
          GraphQLRequestOperation.get,
          ignoreParents: true,
        ); // always format like a get, stop traversing parents
        fields.add("${belongsTo.name} { $parentSelectionSet }");

        // Include the ID key(s) itself in selection set. This is ignored while
        // deserializing response because model will use ID nested in `parentSelectionSet`.
        // However, including the ID in selection set allows subscriptions that
        // filter by these ID to be triggered by these mutations.
        final belongsToKeys = belongsTo.association?.targetNames;
        if (belongsToKeys != null) {
          fields.addAll(belongsToKeys);
        }
      }
    }

    // Get owner fields if present in auth rules
    final ownerFields = (schema.authRules ?? [])
        .map((authRule) => authRule.ownerField)
        .whereNotNull()
        .toList();

    final fieldSelection =
        [...fields, ...ownerFields].join(" "); // e.g. "id name createdAt"

    if (operation == GraphQLRequestOperation.list) {
      return "$items { $fieldSelection } nextToken";
    }

    return fieldSelection;
  }

  String _capitalize(String s) => s[0].toUpperCase() + s.substring(1);

  String _lowerCaseFirstCharacter(String s) =>
      s[0].toLowerCase() + s.substring(1);

  DocumentInputs _buildDocumentInputs(
    ModelSchema schema,
    dynamic operation,
    ModelIdentifier? modelIdentifier,
    Map<String, dynamic> variables, {
    String? overrideQueryFieldType,
    String? indexName,
  }) {
    var upperOutput = "";
    var lowerOutput = "";
    final modelName = _capitalize(schema.name);

    // build inputs based on request operation
    switch (operation) {
      case GraphQLRequestOperation.get:
        final indexes = schema.indexes;
        final modelIndex =
            indexes?.firstWhereOrNull((index) => index.name == null);
        if (modelIndex != null) {
          // custom index(es), e.g.
          // upperOutput: no change (empty string)
          // lowerOutput: (customId: 'abc-123, name: 'lorem')

          // Do not reference variables because scalar types not available in
          // codegen. Instead, just write the value to the document.
          final bLowerOutput = StringBuffer(_openParen);
          for (final field in modelIndex.fields) {
            var value = modelIdentifier!.serializeAsMap()[field];
            if (value is String) {
              value = '"$value"';
            }
            bLowerOutput.write("$field: $value");
            if (field != modelIndex.fields.last) {
              bLowerOutput.write(_commaSpace);
            }
          }
          bLowerOutput.write(_closeParen);
          lowerOutput = bLowerOutput.toString();
        } else {
          // simple, single identifier
          upperOutput = r"($id: ID!)";
          lowerOutput = r"(id: $id)";
        }
      case GraphQLRequestOperation.list:
        upperOutput =
            "(\$filter: Model${modelName}FilterInput, \$limit: Int, \$nextToken: String)";
        lowerOutput =
            r"(filter: $filter, limit: $limit, nextToken: $nextToken)";
      case "listWithIndex":
        // get secondary index query field
        String? queryFieldProp;

        try {
          if (indexName == null) {
            if (((schema.indexes ?? []).first.props).length >= 2) {
              queryFieldProp =
                  (schema.indexes?.first.props[1] as List<String>).first;
            }
          } else {
            queryFieldProp = (schema.indexes
                    ?.firstWhere((index) => index.name == indexName)
                    .props[1] as List<String>)
                .first;
          }
          // ignore: avoid_catches_without_on_clauses
        } catch (e) {
          throw const ApiOperationException(
            "Unable to get query field property from schema",
            recoverySuggestion: "please provide a valid query field",
          );
        }

        upperOutput =
            '(\$filter: Model${modelName}FilterInput, \$limit: Int, \$nextToken: String, \$queryField: ${overrideQueryFieldType ?? 'ID!'}, \$sortDirection: ModelSortDirection)';
        lowerOutput =
            r"(filter: $filter, limit: $limit, nextToken: $nextToken, " +
                (queryFieldProp ?? "") +
                r": $queryField, sortDirection: $sortDirection)";
      case GraphQLRequestOperation.create:
      case GraphQLRequestOperation.update:
      case GraphQLRequestOperation.delete:
        final operationValue = _capitalize(operation.name);

        upperOutput =
            '(\$input: $operationValue${modelName}Input!, \$condition:  Model${modelName}ConditionInput)';
        lowerOutput = r"(input: $input, condition: $condition)";
      case GraphQLRequestOperation.onCreate:
      case GraphQLRequestOperation.onUpdate:
      case GraphQLRequestOperation.onDelete:
        // Only add filter variable when present to support older backends that do not support filtering.
        if (variables.containsKey("filter")) {
          upperOutput = "(\$filter: ModelSubscription${modelName}FilterInput)";
          lowerOutput = r"(filter: $filter)";
        }
      default:
        throw const ApiOperationException(
          "GraphQL Request Operation is currently unsupported",
          recoverySuggestion: "please use a supported GraphQL operation",
        );
    }

    return DocumentInputs(upperOutput, lowerOutput);
  }

  /// Example:
  ///   query getBlog($id: ID!, $content: String) { getBlog(id: $id, content: $content) { id name createdAt } }
  GraphQLRequest<T> buildRequest<T extends Model>({
    required ModelType modelType,
    Model? model,
    ModelIdentifier? modelIdentifier,
    required GraphQLRequestType requestType,
    required GraphQLRequestOperation requestOperation,
    required Map<String, dynamic> variables,
    String? apiName,
    APIAuthorizationType? authorizationMode,
    Map<String, String>? headers,
    int depth = 0,
  }) {
    // retrieve schema from ModelType and validate required properties
    final schema =
        getModelSchemaByModelName(modelType.modelName(), requestOperation);

    // e.g. "Blog" or "Blogs"
    final name = _capitalize(_getName(schema, requestOperation));
    // e.g. "query" or "mutation"
    final requestTypeVal = requestType.name;
    // e.g. "get" or "list"
    final requestOperationVal = requestOperation.name;
    // e.g. "{upper: "($id: ID!)", lower: "(id: $id)"}"
    final documentInputs = _buildDocumentInputs(
      schema,
      requestOperation,
      modelIdentifier,
      variables,
    );
    // e.g. "id name createdAt" - fields to retrieve
    final fields = _getSelectionSetFromModelSchema(schema, requestOperation);
    // e.g. "getBlog"
    final requestName = '$requestOperationVal$name';
    // e.g. query getBlog($id: ID!, $content: String) { getBlog(id: $id, content: $content) { id name createdAt } }
    final document =
        '''$requestTypeVal $requestName${documentInputs.upper} { $requestName${documentInputs.lower} { $fields } }''';
    // e.g "listBlogs"
    final decodePath = requestName;

    return GraphQLRequest<T>(
      document: document,
      variables: variables,
      modelType: modelType,
      decodePath: decodePath,
      apiName: apiName,
      authorizationMode: authorizationMode,
      headers: headers,
    );
  }

  GraphQLRequest<T> buildRequestForSecondaryIndex<T extends Model>({
    required ModelType modelType,
    Model? model,
    ModelIdentifier? modelIdentifier,
    required GraphQLRequestType requestType,
    required dynamic requestOperation,
    required Map<String, dynamic> variables,
    String? apiName,
    APIAuthorizationType? authorizationMode,
    Map<String, String>? headers,
    int depth = 0,
    String? queryField,
    String? sortDirection,
    String? indexName,
    String? overrideQueryFieldType,
    String? customQueryName,
  }) {
    // retrieve schema from ModelT"ype and validate required properties
    final schema = getModelSchemaByModelName(
      modelType.modelName(),
      GraphQLRequestOperation.list,
    );

    // Get secondary index name
    String? secondaryIndexName;

    if (indexName == null) {
      try {
        secondaryIndexName = (schema.indexes ?? []).first.name;
        // ignore: avoid_catches_without_on_clauses
      } catch (e) {
        throw const ApiOperationException(
          "Unable to get secondary index name from schema",
          recoverySuggestion: "please provide a valid index name",
        );
      }
    } else {
      // Search for the index with the given name
      (schema.indexes ?? [])
              .where(
                (index) => index.name == indexName,
              )
              .isEmpty
          ? throw const ApiOperationException(
              "Given secondary index name not found in schema",
              recoverySuggestion: "please provide a valid index name",
            )
          : null;
    }

    // e.g. "query" or "mutation"
    final requestTypeVal = requestType.name;

    // ignore: prefer_single_quotes
    variables["queryField"] = queryField;

    // ignore: prefer_single_quotes
    variables["sortDirection"] = sortDirection;

    // e.g. "{upper: "($id: ID!)", lower: "(id: $id)"}"
    final documentInputs = _buildDocumentInputs(
      schema,
      requestOperation,
      modelIdentifier,
      variables,
      overrideQueryFieldType: overrideQueryFieldType,
      indexName: indexName,
    );

    // e.g. "id name createdAt" - fields to retrieve
    final fields =
        _getSelectionSetFromModelSchema(schema, GraphQLRequestOperation.list);

    // e.g. "getBlog"
    indexName ??= secondaryIndexName;

    if (indexName == null) {
      throw const ApiOperationException(
        "Unable to get secondary index name from schema",
        recoverySuggestion: "please provide a valid index name",
      );
    }

    indexName = "${schema.name}By${indexName.split("By").last}";

    var requestName = "list${_capitalize(indexName)}";

    if (customQueryName != null) {
      requestName = customQueryName;
    }

    // e.g. query getBlog($id: ID!, $content: String) { getBlog(id: $id, content: $content) { id name createdAt } }
    final document =
        """$requestTypeVal $requestName${documentInputs.upper} { $requestName${documentInputs.lower} { $fields } }""";
    // e.g "listBlogs"
    final decodePath = requestName;

    return GraphQLRequest<T>(
      document: document,
      variables: variables,
      modelType: modelType,
      decodePath: decodePath,
      apiName: apiName,
      authorizationMode: authorizationMode,
      headers: headers,
    );
  }

  Map<String, dynamic> buildVariablesForListRequest({
    int? limit,
    String? nextToken,
    Map<String, dynamic>? filter,
  }) {
    return <String, dynamic>{
      "filter": filter,
      "limit": limit,
      "nextToken": nextToken,
    };
  }

  Map<String, dynamic> buildVariablesForMutationRequest({
    required Map<String, dynamic> input,
    Map<String, dynamic>? condition,
  }) {
    return <String, dynamic>{
      "input": input,
      "condition": condition,
    };
  }

  Map<String, dynamic> buildVariablesForSubscriptionRequest({
    required ModelType modelType,
    QueryPredicate? where,
  }) {
    if (where == null) {
      return {};
    }
    final filter = GraphQLRequestFactory.instance
        .queryPredicateToGraphQLFilter(where, modelType);
    return <String, dynamic>{
      "filter": filter,
    };
  }

  /// Translates a `QueryPredicate` to a map representing a GraphQL filter
  /// which AppSync will accept. Exame:
  /// `queryPredicateToGraphQLFilter(Blog.NAME.eq('foo'));` // =>
  /// `{'name': {'eq': 'foo'}}`. In the case of a mutation, it will apply to
  /// the "condition" field rather than "filter."
  Map<String, dynamic>? queryPredicateToGraphQLFilter(
    QueryPredicate? queryPredicate,
    ModelType modelType,
  ) {
    if (queryPredicate == null) {
      return null;
    }
    final schema = getModelSchemaByModelName(modelType.modelName(), null);

    // e.g. { 'name': { 'eq': 'foo }}
    if (queryPredicate is QueryPredicateOperation) {
      final association = schema.fields?[queryPredicate.field]?.association;
      final associatedTargetName = association?.targetNames?.first;
      var fieldName = queryPredicate.field;
      if (queryPredicate.field ==
          "${_lowerCaseFirstCharacter(schema.name)}.$_defaultIdFieldName") {
        // very old schemas where fieldName is "blog.id"
        fieldName = _defaultIdFieldName;
      } else if (associatedTargetName != null) {
        // most schemas from more recent CLI codegen versions

        // when querying for the ID of another model, use the targetName from the schema
        fieldName = associatedTargetName;
      }

      return <String, dynamic>{
        fieldName: _queryFieldOperatorToPartialGraphQLFilter(
          queryPredicate.queryFieldOperator,
        ),
      };
    }

    // and, or, not
    if (queryPredicate is QueryPredicateGroup) {
      // .toShortString() is the same as expected graphql filter strings for all these,
      // so no translation helper required.
      final typeExpression = queryPredicate.type.name;

      // not
      if (queryPredicate.type == QueryPredicateGroupType.not) {
        if (queryPredicate.predicates.length == 1) {
          return <String, dynamic>{
            typeExpression: queryPredicateToGraphQLFilter(
              queryPredicate.predicates[0],
              modelType,
            ),
          };
        }
        // Public not() API only allows 1 condition but QueryPredicateGroup
        // technically allows multiple conditions so explicitly disallow multiple.
        throw const ApiOperationException(
          "Unable to translate not() with multiple conditions.",
        );
      }

      // and, or
      return <String, List<Map<String, dynamic>>>{
        typeExpression: queryPredicate.predicates
            .map(
              (predicate) =>
                  queryPredicateToGraphQLFilter(predicate, modelType)!,
            )
            .toList(),
      };
    }

    throw ApiOperationException(
      "Unable to translate the QueryPredicate $queryPredicate to a GraphQL filter.",
    );
  }

  /// Calls `toJson` on the model and removes key/value pairs that refer to
  /// children when that field is null. The structure of provisioned AppSync `input` type,
  /// such as `CreateBlogInput` does not include nested types, so they will get
  /// an error from AppSync if they are included in mutations.
  ///
  /// When the model has a parent via a belongsTo, the id from the parent is added
  /// as a field similar to "blogID" where the value is `post.blog.id`.
  Map<String, dynamic> buildInputVariableForMutations(
    Model model, {
    required GraphQLRequestOperation operation,
  }) {
    final schema =
        getModelSchemaByModelName(model.getInstanceType().modelName(), null);
    final modelJson = model.toJson();

    // Get the primary key field name from parent schema(s) so it can be taken from
    // JSON. For schemas with `@primaryKey` defined, it will be the first key in
    // the index where name is null. If no such definition in schema, use
    // default primary key "id."
    final allBelongsTo = getBelongsToFieldsFromModelSchema(schema);
    for (final belongsTo in allBelongsTo) {
      final belongsToModelName = belongsTo.name;
      final parentSchema = getModelSchemaByModelName(
        belongsTo.type.ofModelName!,
        operation,
      );
      final primaryKeyIndex = parentSchema.indexes?.firstWhereOrNull(
        (modelIndex) => modelIndex.name == null,
      );

      // Traverse parent schema to get expected JSON strings and remove from JSON.
      // A parent with complex identifier may have multiple keys.
      var belongsToKeys = belongsTo.association?.targetNames;
      if (belongsToKeys == null) {
        // no way to resolve key to write to JSON
        continue;
      }
      belongsToKeys = belongsToKeys;
      var i = 0; // needed to track corresponding index field name
      for (final belongsToKey in belongsToKeys) {
        final parentIdFieldName =
            primaryKeyIndex?.fields[i] ?? _defaultIdFieldName;
        final belongsToValue = (modelJson[belongsToModelName]
            as Map?)?[parentIdFieldName] as String?;

        // Assign the parent ID(s) if the model has a parent.
        if (belongsToValue != null) {
          modelJson[belongsToKey] = belongsToValue;
        }
        i++;
      }
      modelJson.remove(belongsToModelName);
    }

    final ownerFieldNames = (schema.authRules ?? [])
        .map((authRule) => authRule.ownerField)
        .whereNotNull()
        .toSet();
    // Remove some fields from input.
    final fieldsToRemove = schema.fields!.entries
        .where(
          (entry) =>
              // relational fields
              entry.value.association != null ||
              // read-only
              entry.value.isReadOnly ||
              // null values for owner fields on create operations
              (operation == GraphQLRequestOperation.create &&
                  ownerFieldNames.contains(entry.value.name) &&
                  modelJson[entry.value.name] == null),
        )
        .map((entry) => entry.key)
        .toSet();
    modelJson.removeWhere((key, dynamic value) => fieldsToRemove.contains(key));

    return modelJson;
  }
}

/// e.g. `.eq('foo')` => `{ 'eq': 'foo' }`
Map<String, dynamic> _queryFieldOperatorToPartialGraphQLFilter(
  QueryFieldOperator<dynamic> queryFieldOperator,
) {
  final filterExpression = _getGraphQLFilterExpression(queryFieldOperator.type);
  if (queryFieldOperator is QueryFieldOperatorSingleValue) {
    return <String, dynamic>{
      filterExpression: _getSerializedValue(queryFieldOperator.value),
    };
  }
  if (queryFieldOperator is BetweenQueryOperator) {
    return <String, dynamic>{
      filterExpression: <dynamic>[
        _getSerializedValue(queryFieldOperator.start),
        _getSerializedValue(queryFieldOperator.end),
      ],
    };
  }
  if (queryFieldOperator is AttributeExistsQueryOperator) {
    return <String, dynamic>{
      filterExpression: _getSerializedValue(queryFieldOperator.exists),
    };
  }

  throw ApiOperationException(
    "Unable to translate the QueryFieldOperator ${queryFieldOperator.type} to a GraphQL filter.",
  );
}

String _getGraphQLFilterExpression(QueryFieldOperatorType operatorType) {
  final dictionary = {
    QueryFieldOperatorType.equal: "eq",
    QueryFieldOperatorType.not_equal: "ne",
    QueryFieldOperatorType.less_or_equal: "le",
    QueryFieldOperatorType.less_than: "lt",
    QueryFieldOperatorType.greater_than: "gt",
    QueryFieldOperatorType.greater_or_equal: "ge",
    QueryFieldOperatorType.between: "between",
    QueryFieldOperatorType.contains: "contains",
    QueryFieldOperatorType.begins_with: "beginsWith",
    QueryFieldOperatorType.attribute_exists: "attributeExists",
  };
  final result = dictionary[operatorType];
  if (result == null) {
    throw ApiOperationException(
      "$operatorType does not have a defined GraphQL filter string.",
    );
  }
  return result;
}

/// Convert Temporal*, DateTime and enum values to string if needed.
/// Otherwise return unchanged.
dynamic _getSerializedValue(dynamic value) {
  if (value is TemporalDateTime ||
      value is TemporalDate ||
      value is TemporalTime ||
      value is TemporalTimestamp) {
    return value.toString();
  }
  if (value is DateTime) {
    return _getSerializedValue(TemporalDateTime(value));
  }
  if (value is Enum) {
    return value.name;
  }
  return value;
}

Now you can use this like this:

request = CustomModelQueries.listByIndex(
  Todo.classType,
  queryField: todoId,
  sortDirection: "DESC",
  limit: 10,
);

final response = await Amplify.API.query(request: request).response;

from amplify-flutter.

hangoocn avatar hangoocn commented on August 30, 2024

thnx @MarlonJD , i will have a try!

from amplify-flutter.

Related Issues (20)

Recommend Projects

  • React photo React

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

  • Vue.js photo Vue.js

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

  • Typescript photo Typescript

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

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

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

Recommend Topics

  • javascript

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

  • web

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

  • server

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

  • Machine learning

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

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

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

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.