public static IEnumerable ParseAfterFromJsonString()

in src/Core/Resolvers/SqlPaginationUtil.cs [323:456]


        public static IEnumerable<PaginationColumn> ParseAfterFromJsonString(
            string afterJsonString,
            PaginationMetadata paginationMetadata,
            ISqlMetadataProvider sqlMetadataProvider,
            string entityName,
            RuntimeConfigProvider runtimeConfigProvider
            )
        {
            List<PaginationColumn>? paginationCursorColumnsForQuery = new();
            IEnumerable<NextLinkField>? paginationCursorFieldsFromRequest;
            try
            {
                afterJsonString = Base64Decode(afterJsonString);
                paginationCursorFieldsFromRequest = JsonSerializer.Deserialize<IEnumerable<NextLinkField>>(afterJsonString);

                if (paginationCursorFieldsFromRequest is null)
                {
                    throw new ArgumentException("Failed to parse the pagination information from the provided token");
                }

                Dictionary<string, PaginationColumn> exposedFieldNameToBackingColumn = new();
                foreach (NextLinkField field in paginationCursorFieldsFromRequest)
                {
                    // REST calls this function with a non null sqlMetadataProvider
                    // which will get the exposed name for safe messaging in the response.
                    // Since we are looking for pagination columns from the $after query
                    // param, we expect this column to exist as the $after query param
                    // was formed from a previous response with a nextLink. If the nextLink
                    // has been modified and backingColumn is null we throw exception.
                    string backingColumnName = GetBackingColumnName(entityName, field.FieldName, sqlMetadataProvider);
                    if (backingColumnName is null)
                    {
                        throw new DataApiBuilderException(
                            message: $"Pagination token is not well formed because {field.FieldName} is not valid.",
                            statusCode: HttpStatusCode.BadRequest,
                            subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
                    }

                    PaginationColumn pageColumn = new(
                        tableName: "",
                        tableSchema: "",
                        columnName: backingColumnName,
                        value: field.FieldValue,
                        paramName: field.ParamName,
                        direction: field.Direction);
                    paginationCursorColumnsForQuery.Add(pageColumn);
                    // holds exposed name mapped to exposed pagination column
                    exposedFieldNameToBackingColumn.Add(field.FieldName, pageColumn);
                }

                // verify that primary keys is a sub set of after's column names
                // if any primary keys are not contained in after's column names we throw exception
                List<string> primaryKeys = paginationMetadata.Structure!.PrimaryKey();

                if (!paginationMetadata.RequestedGroupBy)
                {
                    // primary key not valid check for groupby ordering.
                    foreach (string pk in primaryKeys)
                    {
                        // REST calls this function with a non null sqlMetadataProvider
                        // which will get the exposed name for safe messaging in the response.
                        // Since we are looking for primary keys we expect these columns to
                        // exist.
                        string exposedFieldName = GetExposedColumnName(entityName, pk, sqlMetadataProvider);
                        if (!exposedFieldNameToBackingColumn.ContainsKey(exposedFieldName))
                        {
                            throw new DataApiBuilderException(
                                message: $"Pagination token is not well formed because it is missing an expected field: {exposedFieldName}",
                                statusCode: HttpStatusCode.BadRequest,
                                subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
                        }
                    }
                }

                // verify that orderby columns for the structure and the after columns
                // match in name and direction
                int orderByColumnCount = 0;
                SqlQueryStructure structure = paginationMetadata.Structure!;
                foreach (OrderByColumn column in structure.OrderByColumns)
                {
                    string exposedFieldName = GetExposedColumnName(entityName, column.ColumnName, sqlMetadataProvider);

                    if (!exposedFieldNameToBackingColumn.ContainsKey(exposedFieldName) ||
                        exposedFieldNameToBackingColumn[exposedFieldName].Direction != column.Direction)
                    {
                        // REST calls this function with a non null sqlMetadataProvider
                        // which will get the exposed name for safe messaging in the response.
                        // Since we are looking for valid orderby columns we expect
                        // these columns to exist.
                        string exposedOrderByFieldName = GetExposedColumnName(entityName, column.ColumnName, sqlMetadataProvider);
                        throw new DataApiBuilderException(
                            message: $"Could not match order by column {exposedOrderByFieldName} with a column in the pagination token with the same name and direction.",
                            statusCode: HttpStatusCode.BadRequest,
                            subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
                    }

                    orderByColumnCount++;
                }

                // the check above validates that all orderby columns are matched with after columns
                // also validate that there are no extra after columns
                if (exposedFieldNameToBackingColumn.Count != orderByColumnCount)
                {
                    throw new ArgumentException("After token contains extra columns not present in order by columns.");
                }
            }
            catch (Exception e) when (
                e is InvalidCastException ||
                e is ArgumentException ||
                e is ArgumentNullException ||
                e is FormatException ||
                e is System.Text.DecoderFallbackException ||
                e is JsonException ||
                e is NotSupportedException
                )
            {
                // Possible sources of exceptions:
                // stringObject cannot be converted to string
                // afterPlainText cannot be successfully decoded
                // afterJsonString cannot be deserialized
                // keys of afterDeserialized do not correspond to the primary key
                // values given for the primary keys are of incorrect format
                // duplicate column names in the after token and / or the orderby columns
                string errorMessage = runtimeConfigProvider.GetConfig().IsDevelopmentMode() ? $"{e.Message}\n{e.StackTrace}" :
                    $"{afterJsonString} is not a valid pagination token.";
                throw new DataApiBuilderException(
                    message: errorMessage,
                    statusCode: HttpStatusCode.BadRequest,
                    subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest,
                    innerException: e);
            }

            return paginationCursorColumnsForQuery;
        }