diff --git a/Tzkt.Api/Controllers/BigMapsController.cs b/Tzkt.Api/Controllers/BigMapsController.cs index 16a7b1bfe..99a2fe9be 100644 --- a/Tzkt.Api/Controllers/BigMapsController.cs +++ b/Tzkt.Api/Controllers/BigMapsController.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.Json; using System.Text.RegularExpressions; @@ -8,6 +7,7 @@ using Netezos.Encoding; using Tzkt.Api.Models; using Tzkt.Api.Repositories; +using Tzkt.Api.Services.Output; namespace Tzkt.Api.Controllers { @@ -15,11 +15,13 @@ namespace Tzkt.Api.Controllers [Route("v1/bigmaps")] public class BigMapsController : ControllerBase { - private readonly BigMapsRepository BigMaps; + readonly BigMapsRepository BigMaps; + readonly OutputCachingService OutputCache; - public BigMapsController(BigMapsRepository bigMaps) + public BigMapsController(BigMapsRepository bigMaps, OutputCachingService outputCache) { BigMaps = bigMaps; + OutputCache = outputCache; } /// @@ -30,9 +32,19 @@ public BigMapsController(BigMapsRepository bigMaps) /// /// [HttpGet("count")] - public Task GetBigMapsCount() + public async Task> GetBigMapsCount() { - return BigMaps.GetCount(); + var queryString = Request.Path.Value; + + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) + { + return File(cachedResponse.Bytes, "application/json"); + } + + var res = await BigMaps.GetCount(); + + OutputCache.Set(queryString, res); + return Ok(res); } /// @@ -70,29 +82,44 @@ public async Task>> GetBigMaps( return new BadRequest($"{nameof(sort)}", "Sorting by the specified field is not allowed."); #endregion - if (select == null) - return Ok(await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, micheline)); + var queryString = OutputCacheKeysProvider.BuildQuery(Request.Path.Value,("contract", contract), ("path", path), ("tags", tags), + ("active", active), ("lastLevel", lastLevel), ("select", select), ("sort", sort), ("offset", offset), ("limit", limit), ("micheline", micheline)); - if (select.Values != null) + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) { - if (select.Values.Length == 1) - return Ok(await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, select.Values[0], micheline)); - else - return Ok(await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, select.Values, micheline)); + return File(cachedResponse.Bytes, "application/json"); } + + object res; + + if (select == null) + res = await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, micheline); else { - if (select.Fields.Length == 1) - return Ok(await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, select.Fields[0], micheline)); + if (select.Values != null) + { + if (select.Values.Length == 1) + res = await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, select.Values[0], micheline); + else + res = await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, select.Values, micheline); + } else { - return Ok(new SelectionResponse + if (select.Fields.Length == 1) + res = await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, select.Fields[0], micheline); + else { - Cols = select.Fields, - Rows = await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, select.Fields, micheline) - }); + res = new SelectionResponse + { + Cols = select.Fields, + Rows = await BigMaps.Get(contract, path, tags, active, lastLevel, sort, offset, limit, select.Fields, micheline) + }; + } } } + + OutputCache.Set(queryString, res); + return Ok(res); } /// @@ -134,11 +161,26 @@ public async Task>> GetBigMapUpdates( if (sort != null && !sort.Validate("id", "level")) return new BadRequest($"{nameof(sort)}", "Sorting by the specified field is not allowed."); #endregion + + var queryString = OutputCacheKeysProvider.BuildQuery(Request.Path.Value,("bigmap", bigmap), ("path", path), ("contract", contract), ("tags", tags), + ("action", action), ("value", value), ("level", level), ("timestamp", timestamp), ("sort", sort), ("offset", offset), ("limit", limit), ("micheline", micheline)); - if (path == null && contract == null && tags == null) - return Ok(await BigMaps.GetUpdates(bigmap, action, value, level, timestamp, sort, offset, limit, micheline)); + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) + { + return File(cachedResponse.Bytes, "application/json"); + } + + object res; - return Ok(await BigMaps.GetUpdates(bigmap, path, contract, action, value, tags, level, timestamp, sort, offset, limit, micheline)); + if (path == null && contract == null && tags == null) + res = await BigMaps.GetUpdates(bigmap, action, value, level, timestamp, sort, offset, limit, micheline); + else + { + res = await BigMaps.GetUpdates(bigmap, path, contract, action, value, tags, level, timestamp, sort, offset, limit, micheline); + } + + OutputCache.Set(queryString, res); + return Ok(res); } /// @@ -151,11 +193,21 @@ public async Task>> GetBigMapUpdates( /// Format of the bigmap key and value type: `0` - JSON, `2` - Micheline /// [HttpGet("{id:int}")] - public Task GetBigMapById( + public async Task> GetBigMapById( [Min(0)] int id, MichelineFormat micheline = MichelineFormat.Json) { - return BigMaps.Get(id, micheline); + var queryString = OutputCacheKeysProvider.BuildQuery(Request.Path.Value, ("micheline", micheline)); + + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) + { + return File(cachedResponse.Bytes, "application/json"); + } + + var res = await BigMaps.Get(id, micheline); + + OutputCache.Set(queryString, res); + return Ok(res); } /// @@ -167,9 +219,19 @@ public Task GetBigMapById( /// Bigmap Id /// [HttpGet("{id:int}/type")] - public Task GetBigMapType([Min(0)] int id) + public async Task> GetBigMapType([Min(0)] int id) { - return BigMaps.GetMicheType(id); + var queryString = Request.Path.Value; + + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) + { + return File(cachedResponse.Bytes, "application/json"); + } + + var res = await BigMaps.GetMicheType(id); + + OutputCache.Set(queryString, res); + return Ok(res); } /// @@ -208,30 +270,42 @@ public async Task>> GetKeys( if (sort != null && !sort.Validate("id", "firstLevel", "lastLevel", "updates")) return new BadRequest(nameof(sort), "Sorting by the specified field is not allowed."); #endregion + + var queryString = OutputCacheKeysProvider.BuildQuery(Request.Path.Value,("active", active), ("key", key), + ("value", value), ("lastLevel", lastLevel), ("select", select), ("sort", sort), ("offset", offset), ("limit", limit), ("micheline", micheline)); + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) + { + return File(cachedResponse.Bytes, "application/json"); + } + + object res; + if (select == null) - return Ok(await BigMaps.GetKeys(id, active, key, value, lastLevel, sort, offset, limit, micheline)); - - if (select.Values != null) + res = await BigMaps.GetKeys(id, active, key, value, lastLevel, sort, offset, limit, micheline); + else if (select.Values != null) { if (select.Values.Length == 1) - return Ok(await BigMaps.GetKeys(id, active, key, value, lastLevel, sort, offset, limit, select.Values[0], micheline)); + res = await BigMaps.GetKeys(id, active, key, value, lastLevel, sort, offset, limit, select.Values[0], micheline); else - return Ok(await BigMaps.GetKeys(id, active, key, value, lastLevel, sort, offset, limit, select.Values, micheline)); + res = await BigMaps.GetKeys(id, active, key, value, lastLevel, sort, offset, limit, select.Values, micheline); } else { if (select.Fields.Length == 1) - return Ok(await BigMaps.GetKeys(id, active, key, value, lastLevel, sort, offset, limit, select.Fields[0], micheline)); + res = await BigMaps.GetKeys(id, active, key, value, lastLevel, sort, offset, limit, select.Fields[0], micheline); else { - return Ok(new SelectionResponse + res = new SelectionResponse { Cols = select.Fields, Rows = await BigMaps.GetKeys(id, active, key, value, lastLevel, sort, offset, limit, select.Fields, micheline) - }); + }; } } + + OutputCache.Set(queryString, res); + return Ok(res); } /// @@ -253,20 +327,30 @@ public async Task> GetKey( { try { - if (Regex.IsMatch(key, @"^expr[0-9A-z]{50}$")) - return Ok(await BigMaps.GetKeyByHash(id, key, micheline)); + var queryString = OutputCacheKeysProvider.BuildQuery(Request.Path.Value, ("micheline", micheline)); - using var doc = JsonDocument.Parse(WrapKey(key)); - return Ok(await BigMaps.GetKey(id, doc.RootElement.GetRawText(), micheline)); + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) + { + return File(cachedResponse.Bytes, "application/json"); + } + + object res; + + if (Regex.IsMatch(key, @"^expr[0-9A-z]{50}$")) + res = await BigMaps.GetKeyByHash(id, key, micheline); + else + { + using var doc = JsonDocument.Parse(WrapKey(key)); + res = await BigMaps.GetKey(id, doc.RootElement.GetRawText(), micheline); + } + + OutputCache.Set(queryString, res); + return Ok(res); } catch (JsonException) { return new BadRequest(nameof(key), "invalid json value"); } - catch - { - throw; - } } /// @@ -299,20 +383,30 @@ public async Task>> GetKeyUpdates( try { - if (Regex.IsMatch(key, @"^expr[0-9A-z]{50}$")) - return Ok(await BigMaps.GetKeyByHashUpdates(id, key, sort, offset, limit, micheline)); + var queryString = OutputCacheKeysProvider.BuildQuery(Request.Path.Value,("sort", sort), ("offset", offset), ("limit", limit), ("micheline", micheline)); - using var doc = JsonDocument.Parse(WrapKey(key)); - return Ok(await BigMaps.GetKeyUpdates(id, doc.RootElement.GetRawText(), sort, offset, limit, micheline)); + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) + { + return File(cachedResponse.Bytes, "application/json"); + } + + object res; + + if (Regex.IsMatch(key, @"^expr[0-9A-z]{50}$")) + res = await BigMaps.GetKeyByHashUpdates(id, key, sort, offset, limit, micheline); + else + { + using var doc = JsonDocument.Parse(WrapKey(key)); + res = await BigMaps.GetKeyUpdates(id, doc.RootElement.GetRawText(), sort, offset, limit, micheline); + } + + OutputCache.Set(queryString, res); + return Ok(res); } catch (JsonException) { return new BadRequest(nameof(key), "invalid json value"); } - catch - { - throw; - } } /// @@ -352,29 +446,44 @@ public async Task>> GetHistoricalK return new BadRequest(nameof(sort), "Sorting by the specified field is not allowed."); #endregion - if (select == null) - return Ok(await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, micheline)); + var queryString = OutputCacheKeysProvider.BuildQuery(Request.Path.Value,("active", active), ("key", key), + ("value", value), ("select", select), ("sort", sort), ("offset", offset), ("limit", limit), ("micheline", micheline)); - if (select.Values != null) + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) { - if (select.Values.Length == 1) - return Ok(await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, select.Values[0], micheline)); - else - return Ok(await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, select.Values, micheline)); + return File(cachedResponse.Bytes, "application/json"); } + + object res; + + if (select == null) + res = await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, micheline); else { - if (select.Fields.Length == 1) - return Ok(await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, select.Fields[0], micheline)); + if (select.Values != null) + { + if (select.Values.Length == 1) + res = await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, select.Values[0], micheline); + else + res = await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, select.Values, micheline); + } else { - return Ok(new SelectionResponse + if (select.Fields.Length == 1) + res = await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, select.Fields[0], micheline); + else { - Cols = select.Fields, - Rows = await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, select.Fields, micheline) - }); + res = new SelectionResponse + { + Cols = select.Fields, + Rows = await BigMaps.GetHistoricalKeys(id, level, active, key, value, sort, offset, limit, select.Fields, micheline) + }; + } } } + + OutputCache.Set(queryString, res); + return Ok(res); } /// @@ -398,20 +507,30 @@ public async Task> GetKey( { try { - if (Regex.IsMatch(key, @"^expr[0-9A-z]{50}$")) - return Ok(await BigMaps.GetHistoricalKeyByHash(id, level, key, micheline)); + var queryString = OutputCacheKeysProvider.BuildQuery(Request.Path.Value,("level", level), ("key", key), ("micheline", micheline)); - using var doc = JsonDocument.Parse(WrapKey(key)); - return Ok(await BigMaps.GetHistoricalKey(id, level, doc.RootElement.GetRawText(), micheline)); + if (OutputCache.TryGetFromCache(HttpContext, queryString, out var cachedResponse)) + { + return File(cachedResponse.Bytes, "application/json"); + } + + object res; + + if (Regex.IsMatch(key, @"^expr[0-9A-z]{50}$")) + res = await BigMaps.GetHistoricalKeyByHash(id, level, key, micheline); + else + { + using var doc = JsonDocument.Parse(WrapKey(key)); + res = await BigMaps.GetHistoricalKey(id, level, doc.RootElement.GetRawText(), micheline); + } + + OutputCache.Set(queryString, res); + return Ok(res); } catch (JsonException) { return new BadRequest(nameof(key), "invalid json value"); } - catch - { - throw; - } } string WrapKey(string key) diff --git a/Tzkt.Api/Parameters/AccountParameter.cs b/Tzkt.Api/Parameters/AccountParameter.cs index 4a002eddc..28189e14a 100644 --- a/Tzkt.Api/Parameters/AccountParameter.cs +++ b/Tzkt.Api/Parameters/AccountParameter.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using System.Text; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using NJsonSchema.Annotations; @@ -7,7 +9,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(AccountBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class AccountParameter + public class AccountParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -74,5 +76,51 @@ public class AccountParameter [JsonIgnore] public bool NiHasNull { get; set; } + + + public string Normalize(string name) + { + var sb = new StringBuilder(); + + if (Eq != null) + { + sb.Append($"{name}.eq={Eq}&"); + } + + if (Ne != null) + { + sb.Append($"{name}.ne={Ne}&"); + } + + if (In != null && In.Any()) + { + sb.Append($"{name}.in={string.Join(",", In.OrderBy(x => x))}&"); + } + + if (Ni != null && Ni.Any()) + { + sb.Append($"{name}.ni={string.Join(",", Ni.OrderBy(x => x))}&"); + } + + if (Eqx != null) + { + sb.Append($"{name}.eqx={Eqx}&"); + } + + if (Nex != null) + { + sb.Append($"{name}.nex={Nex}&"); + } + + if (Null != null) + { + sb.Append($"{name}.null={Null}&"); + } + + sb.Append($"{name}.NiHasNull={NiHasNull}&"); + sb.Append($"{name}.InHasNull={InHasNull}&"); + + return sb.ToString(); + } } } diff --git a/Tzkt.Api/Parameters/AccountTypeParameter.cs b/Tzkt.Api/Parameters/AccountTypeParameter.cs index 00ab361f2..dd9b3fe3a 100644 --- a/Tzkt.Api/Parameters/AccountTypeParameter.cs +++ b/Tzkt.Api/Parameters/AccountTypeParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(AccountTypeBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "user,contract,delegate")] - public class AccountTypeParameter + public class AccountTypeParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -25,5 +25,10 @@ public class AccountTypeParameter /// [JsonSchemaType(typeof(string))] public int? Ne { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/AnyOfParameter.cs b/Tzkt.Api/Parameters/AnyOfParameter.cs index 029fcbe04..b555cd939 100644 --- a/Tzkt.Api/Parameters/AnyOfParameter.cs +++ b/Tzkt.Api/Parameters/AnyOfParameter.cs @@ -7,10 +7,14 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(AnyOfBinder))] [JsonSchemaType(typeof(string))] [JsonSchemaExtensionData("x-tzkt-extension", "anyof-parameter")] - public class AnyOfParameter + public class AnyOfParameter : INormalizable { public IEnumerable Fields { get; set; } public int Value { get; set; } + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } \ No newline at end of file diff --git a/Tzkt.Api/Parameters/BakingRightStatusParameter.cs b/Tzkt.Api/Parameters/BakingRightStatusParameter.cs index ed828ef74..2b29cbd67 100644 --- a/Tzkt.Api/Parameters/BakingRightStatusParameter.cs +++ b/Tzkt.Api/Parameters/BakingRightStatusParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(BakingRightStatusBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "future,realized,uncovered,missed")] - public class BakingRightStatusParameter + public class BakingRightStatusParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -25,5 +25,10 @@ public class BakingRightStatusParameter /// [JsonSchemaType(typeof(string))] public int? Ne { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/BakingRightTypeParameter.cs b/Tzkt.Api/Parameters/BakingRightTypeParameter.cs index 49ebaee1e..25beb7e3c 100644 --- a/Tzkt.Api/Parameters/BakingRightTypeParameter.cs +++ b/Tzkt.Api/Parameters/BakingRightTypeParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(BakingRightTypeBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "baking,endorsing")] - public class BakingRightTypeParameter + public class BakingRightTypeParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -25,5 +25,10 @@ public class BakingRightTypeParameter /// [JsonSchemaType(typeof(string))] public int? Ne { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/Base/INormalizable.cs b/Tzkt.Api/Parameters/Base/INormalizable.cs new file mode 100644 index 000000000..2270b1870 --- /dev/null +++ b/Tzkt.Api/Parameters/Base/INormalizable.cs @@ -0,0 +1,7 @@ +namespace Tzkt.Api +{ + public interface INormalizable + { + public string Normalize(string name); + } +} \ No newline at end of file diff --git a/Tzkt.Api/Parameters/BigMapActionParameter.cs b/Tzkt.Api/Parameters/BigMapActionParameter.cs index f88fac50f..781b5e583 100644 --- a/Tzkt.Api/Parameters/BigMapActionParameter.cs +++ b/Tzkt.Api/Parameters/BigMapActionParameter.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using System.Text; using Microsoft.AspNetCore.Mvc; using NJsonSchema.Annotations; @@ -7,7 +9,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(BigMapActionBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "allocate,add_key,update_key,remove_key,remove")] - public class BigMapActionParameter + public class BigMapActionParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -44,5 +46,32 @@ public class BigMapActionParameter /// [JsonSchemaType(typeof(List))] public List Ni { get; set; } + + public string Normalize(string name) + { + var sb = new StringBuilder(); + + if (Eq != null) + { + sb.Append($"{name}.eq={Eq}&"); + } + + if (Ne != null) + { + sb.Append($"{name}.ne={Ne}&"); + } + + if (In != null && In.Any()) + { + sb.Append($"{name}.in={string.Join(",", In.OrderBy(x => x))}&"); + } + + if (Ni != null && Ni.Any()) + { + sb.Append($"{name}.ni={string.Join(",", Ni.OrderBy(x => x))}&"); + } + + return sb.ToString(); + } } } diff --git a/Tzkt.Api/Parameters/BigMapTagsParameter.cs b/Tzkt.Api/Parameters/BigMapTagsParameter.cs index 467a0472a..f772a805a 100644 --- a/Tzkt.Api/Parameters/BigMapTagsParameter.cs +++ b/Tzkt.Api/Parameters/BigMapTagsParameter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text; using Microsoft.AspNetCore.Mvc; using NJsonSchema.Annotations; @@ -7,7 +8,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(BigMapTagsBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "metadata,token_metadata,ledger")] - public class BigMapTagsParameter + public class BigMapTagsParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -36,5 +37,27 @@ public class BigMapTagsParameter /// [JsonSchemaType(typeof(List))] public int? All { get; set; } + + public string Normalize(string name) + { + var sb = new StringBuilder(); + + if (Eq != null) + { + sb.Append($"{name}.eq={Eq}&"); + } + + if (Any != null) + { + sb.Append($"{name}.any={Any}&"); + } + + if (All != null) + { + sb.Append($"{name}.all={All}&"); + } + + return sb.ToString(); + } } } diff --git a/Tzkt.Api/Parameters/BoolParameter.cs b/Tzkt.Api/Parameters/BoolParameter.cs index 909d17056..a9a0a582d 100644 --- a/Tzkt.Api/Parameters/BoolParameter.cs +++ b/Tzkt.Api/Parameters/BoolParameter.cs @@ -5,7 +5,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(BoolBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class BoolParameter + public class BoolParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=true` is the same as `param=true`). \ @@ -22,5 +22,10 @@ public class BoolParameter /// Example: `?active.null` or `?active.null=false`. /// public bool? Null { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/ContractKindParameter.cs b/Tzkt.Api/Parameters/ContractKindParameter.cs index a58cf1355..4a1889a1e 100644 --- a/Tzkt.Api/Parameters/ContractKindParameter.cs +++ b/Tzkt.Api/Parameters/ContractKindParameter.cs @@ -7,7 +7,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(ContractKindBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "delegator_contract,smart_contract")] - public class ContractKindParameter + public class ContractKindParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -44,5 +44,10 @@ public class ContractKindParameter /// [JsonSchemaType(typeof(List))] public List Ni { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/ContractTagsParameter.cs b/Tzkt.Api/Parameters/ContractTagsParameter.cs index 1fa28171f..d98bd01c6 100644 --- a/Tzkt.Api/Parameters/ContractTagsParameter.cs +++ b/Tzkt.Api/Parameters/ContractTagsParameter.cs @@ -7,7 +7,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(ContractTagsBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "fa1,fa12,fa2")] - public class ContractTagsParameter + public class ContractTagsParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -36,5 +36,10 @@ public class ContractTagsParameter /// [JsonSchemaType(typeof(List))] public int? All { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/DateTimeParameter.cs b/Tzkt.Api/Parameters/DateTimeParameter.cs index 97f139679..3e7495f6e 100644 --- a/Tzkt.Api/Parameters/DateTimeParameter.cs +++ b/Tzkt.Api/Parameters/DateTimeParameter.cs @@ -7,7 +7,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(DateTimeBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class DateTimeParameter + public class DateTimeParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=2020-01-01` is the same as `param=2020-01-01`). \ @@ -72,5 +72,10 @@ public class DateTimeParameter /// Example: `?timestamp.ni=2020-02-20,2020-02-21`. /// public List Ni { get; set; } + + public string Normalize(string name) + { + throw new NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/ExpressionParameter.cs b/Tzkt.Api/Parameters/ExpressionParameter.cs index 3312bdcf8..8d05fb750 100644 --- a/Tzkt.Api/Parameters/ExpressionParameter.cs +++ b/Tzkt.Api/Parameters/ExpressionParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(ExpressionBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class ExpressionParameter + public class ExpressionParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -43,5 +43,10 @@ public class ExpressionParameter #region operators public static implicit operator ExpressionParameter(string value) => new() { Eq = value }; #endregion + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/Int32ExParameter.cs b/Tzkt.Api/Parameters/Int32ExParameter.cs index db384f6ff..0e8ead3e5 100644 --- a/Tzkt.Api/Parameters/Int32ExParameter.cs +++ b/Tzkt.Api/Parameters/Int32ExParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(Int32ExBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class Int32ExParameter + public class Int32ExParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -95,5 +95,10 @@ public class Int32ExParameter /// Example: `?nonce.null` or `?nonce.null=false`. /// public bool? Null { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/Int32NullParameter.cs b/Tzkt.Api/Parameters/Int32NullParameter.cs index 2403d4bb7..87fbe7b49 100644 --- a/Tzkt.Api/Parameters/Int32NullParameter.cs +++ b/Tzkt.Api/Parameters/Int32NullParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(Int32NullBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class Int32NullParameter + public class Int32NullParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -79,5 +79,10 @@ public class Int32NullParameter /// Example: `?nonce.null` or `?nonce.null=false`. /// public bool? Null { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/Int32Parameter.cs b/Tzkt.Api/Parameters/Int32Parameter.cs index 8a3ca762b..42f7bc94a 100644 --- a/Tzkt.Api/Parameters/Int32Parameter.cs +++ b/Tzkt.Api/Parameters/Int32Parameter.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using System.Text; using Microsoft.AspNetCore.Mvc; using NJsonSchema.Annotations; @@ -6,7 +8,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(Int32Binder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class Int32Parameter + public class Int32Parameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -71,5 +73,52 @@ public class Int32Parameter /// Example: `?level.ni=12,14,52,69`. /// public List Ni { get; set; } + + public string Normalize(string name) + { + var sb = new StringBuilder(); + + if (Eq != null) + { + sb.Append($"{name}.eq={Eq}&"); + } + + if (Ne != null) + { + sb.Append($"{name}.ne={Ne}&"); + } + + if (Gt != null) + { + sb.Append($"{name}.gt={Gt}&"); + } + + if (Ge != null) + { + sb.Append($"{name}.ge={Ge}&"); + } + + if (Lt != null) + { + sb.Append($"{name}.lt={Lt}&"); + } + + if (Le != null) + { + sb.Append($"{name}.le={Le}&"); + } + + if (In != null && In.Any()) + { + sb.Append($"{name}.in={string.Join(",", In.OrderBy(x => x))}&"); + } + + if (Ni != null && Ni.Any()) + { + sb.Append($"{name}.ni={string.Join(",", Ni.OrderBy(x => x))}&"); + } + + return sb.ToString(); + } } } diff --git a/Tzkt.Api/Parameters/Int64ExParameter.cs b/Tzkt.Api/Parameters/Int64ExParameter.cs index ad1b628bd..7bc865094 100644 --- a/Tzkt.Api/Parameters/Int64ExParameter.cs +++ b/Tzkt.Api/Parameters/Int64ExParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(Int64ExBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class Int64ExParameter + public class Int64ExParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -95,5 +95,10 @@ public class Int64ExParameter /// Example: `?nonce.null` or `?nonce.null=false`. /// public bool? Null { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/Int64NullParameter.cs b/Tzkt.Api/Parameters/Int64NullParameter.cs index 482c008f7..88ef80e86 100644 --- a/Tzkt.Api/Parameters/Int64NullParameter.cs +++ b/Tzkt.Api/Parameters/Int64NullParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(Int64NullBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class Int64NullParameter + public class Int64NullParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -79,5 +79,10 @@ public class Int64NullParameter /// Example: `?nonce.null` or `?nonce.null=false`. /// public bool? Null { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/Int64Parameter.cs b/Tzkt.Api/Parameters/Int64Parameter.cs index f78dbdf29..c36b443a8 100644 --- a/Tzkt.Api/Parameters/Int64Parameter.cs +++ b/Tzkt.Api/Parameters/Int64Parameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(Int64Binder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class Int64Parameter + public class Int64Parameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -71,5 +71,10 @@ public class Int64Parameter /// Example: `?level.ni=12,14,52,69`. /// public List Ni { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/JsonParameter.cs b/Tzkt.Api/Parameters/JsonParameter.cs index 1dfe6102d..8b9c62056 100644 --- a/Tzkt.Api/Parameters/JsonParameter.cs +++ b/Tzkt.Api/Parameters/JsonParameter.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using System.Text; using Microsoft.AspNetCore.Mvc; using NJsonSchema.Annotations; @@ -8,7 +10,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(JsonBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "json-parameter")] - public class JsonParameter + public class JsonParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -134,5 +136,108 @@ public class JsonParameter [JsonSchemaType(typeof(bool))] [JsonSchemaExtensionData("x-tzkt-extension", "json-parameter")] public List<(JsonPath[], bool)> Null { get; set; } + + public string Normalize(string name) + { + var sb = new StringBuilder(); + + if (Eq != null) + { + foreach (var (path, value) in Eq) + { + sb.Append($"{name}.{Normalize(path)}.eq={value}&"); + } + } + + if (Ne != null) + { + foreach (var (path, value) in Ne) + { + sb.Append($"{name}.{Normalize(path)}.ne={value}&"); + } + } + + if (Gt != null) + { + foreach (var (path, value) in Gt) + { + sb.Append($"{name}.{Normalize(path)}.gt={value}&"); + } + } + + if (Ge != null) + { + foreach (var (path, value) in Ge) + { + sb.Append($"{name}.{Normalize(path)}.ge={value}&"); + } + } + + if (Lt != null) + { + foreach (var (path, value) in Lt) + { + sb.Append($"{name}.{Normalize(path)}.lt={value}&"); + + } + } + + if (Le != null) + { + foreach (var (path, value) in Le) + { + sb.Append($"{name}.{Normalize(path)}.le={value}&"); + + } + } + + if (As != null) + { + foreach (var (path, value) in As) + { + sb.Append($"{name}.{Normalize(path)}.as={value}&"); + } + } + + if (Un != null) + { + foreach (var (path, value) in Un) + { + sb.Append($"{name}.{Normalize(path)}.un={value}&"); + + } + } + + if (In != null) + { + foreach (var (path, values) in In) + { + sb.Append($"{name}.{Normalize(path)}.in={string.Join(",",values.OrderBy(x => x))}&"); + } + } + + if (Ni != null) + { + foreach (var (path, values) in Ni) + { + sb.Append($"{name}.{Normalize(path)}.ni={string.Join(",",values.OrderBy(x => x))}&"); + } + } + + if (Null != null) + { + foreach (var (path, value) in Null) + { + sb.Append($"{name}.{Normalize(path)}.null={value}&"); + } + } + + return sb.ToString(); + } + + static string Normalize(JsonPath[] jsonPaths) + { + return string.Join(".", jsonPaths.Select(x => x.Type > JsonPathType.Key ? $"[{x.Value ?? "*"}]" : x.Value)); + } } -} +} \ No newline at end of file diff --git a/Tzkt.Api/Parameters/MigrationKindParameter.cs b/Tzkt.Api/Parameters/MigrationKindParameter.cs index dabfe597a..03a5c717d 100644 --- a/Tzkt.Api/Parameters/MigrationKindParameter.cs +++ b/Tzkt.Api/Parameters/MigrationKindParameter.cs @@ -7,7 +7,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(MigrationKindBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "bootstrap,activate_delegate,airdrop,proposal_invoice,code_change,origination,subsidy")] - public class MigrationKindParameter + public class MigrationKindParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -44,5 +44,10 @@ public class MigrationKindParameter /// [JsonSchemaType(typeof(List))] public List Ni { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/NatParameter.cs b/Tzkt.Api/Parameters/NatParameter.cs index b8fc3d580..9caa530b7 100644 --- a/Tzkt.Api/Parameters/NatParameter.cs +++ b/Tzkt.Api/Parameters/NatParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(NatBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class NatParameter + public class NatParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -71,5 +71,10 @@ public class NatParameter /// Example: `?level.ni=12,14,52,69`. /// public List Ni { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/OffsetParameter.cs b/Tzkt.Api/Parameters/OffsetParameter.cs index 30543ff1f..4879b6b44 100644 --- a/Tzkt.Api/Parameters/OffsetParameter.cs +++ b/Tzkt.Api/Parameters/OffsetParameter.cs @@ -1,11 +1,12 @@ -using Microsoft.AspNetCore.Mvc; +using System.Text; +using Microsoft.AspNetCore.Mvc; using NJsonSchema.Annotations; namespace Tzkt.Api { [ModelBinder(BinderType = typeof(OffsetBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class OffsetParameter + public class OffsetParameter : INormalizable { /// /// **Elements** offset mode (optional, i.e. `offset.el=123` is the same as `offset=123`). \ @@ -33,5 +34,25 @@ public class OffsetParameter public long? Cr { get; set; } public static implicit operator OffsetParameter(int offset) => new() { El = offset }; + public string Normalize(string name) + { + var sb = new StringBuilder(); + if (El != null) + { + sb.Append($"offset.el={El}&"); + } + + if (Pg != null) + { + sb.Append($"offset.pg={Pg}&"); + } + + if (Cr != null) + { + sb.Append($"offset.cr={Cr}"); + } + + return sb.ToString(); + } } } diff --git a/Tzkt.Api/Parameters/OperationStatusParameter.cs b/Tzkt.Api/Parameters/OperationStatusParameter.cs index 75dcc12e7..3347202cc 100644 --- a/Tzkt.Api/Parameters/OperationStatusParameter.cs +++ b/Tzkt.Api/Parameters/OperationStatusParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(OperationStatusBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "applied,failed,backtracked,skipped")] - public class OperationStatusParameter + public class OperationStatusParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=applied` is the same as `param=applied`). \ @@ -25,5 +25,10 @@ public class OperationStatusParameter /// [JsonSchemaType(typeof(string))] public int? Ne { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/Pagination.cs b/Tzkt.Api/Parameters/Pagination.cs index 3c2404c5e..424f14c9d 100644 --- a/Tzkt.Api/Parameters/Pagination.cs +++ b/Tzkt.Api/Parameters/Pagination.cs @@ -2,7 +2,7 @@ namespace Tzkt.Api { - public class Pagination + public class Pagination : INormalizable { /// /// Sorts items (asc or desc) by the specified field. @@ -22,5 +22,10 @@ public class Pagination /// [Range(0, 10000)] public int limit { get; set; } = 100; + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/ProtocolParameter.cs b/Tzkt.Api/Parameters/ProtocolParameter.cs index 55e1ce201..b48da79d8 100644 --- a/Tzkt.Api/Parameters/ProtocolParameter.cs +++ b/Tzkt.Api/Parameters/ProtocolParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(ProtocolBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class ProtocolParameter + public class ProtocolParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -39,5 +39,10 @@ public class ProtocolParameter /// Example: `?sender.ni=PsCARTHAGaz,PsBabyM1eUX`. /// public List Ni { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/SelectParameter.cs b/Tzkt.Api/Parameters/SelectParameter.cs index 13a938da3..05b88f901 100644 --- a/Tzkt.Api/Parameters/SelectParameter.cs +++ b/Tzkt.Api/Parameters/SelectParameter.cs @@ -5,7 +5,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(SelectBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class SelectParameter + public class SelectParameter : INormalizable { /// /// **Fields** selection mode (optional, i.e. `select.fields=balance` is the same as `select=balance`). \ @@ -22,5 +22,11 @@ public class SelectParameter /// Example: `?select.values=address,balance` => `[ [ "asd", 10 ] ]`. /// public string[] Values { get; set; } + + public string Normalize(string name) + { + //We can't order values, but perhaps we can order fields. + return Values != null ? $"select.values={string.Join(",", Values)}&" : $"select.fields={string.Join(",", Fields)}&"; + } } } diff --git a/Tzkt.Api/Parameters/Selection.cs b/Tzkt.Api/Parameters/Selection.cs index 2b9b20a0a..f61966a5e 100644 --- a/Tzkt.Api/Parameters/Selection.cs +++ b/Tzkt.Api/Parameters/Selection.cs @@ -1,6 +1,6 @@ namespace Tzkt.Api { - public class Selection + public class Selection : INormalizable { /// /// Specify a comma-separated list of fields to include into response or leave it undefined to get default set of fields. @@ -10,5 +10,10 @@ public class Selection /// Click on the parameter to expand the details. /// public SelectionParameter select { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/SelectionParameter.cs b/Tzkt.Api/Parameters/SelectionParameter.cs index ab9f005d7..15fa8c33c 100644 --- a/Tzkt.Api/Parameters/SelectionParameter.cs +++ b/Tzkt.Api/Parameters/SelectionParameter.cs @@ -8,7 +8,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(SelectionBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class SelectionParameter + public class SelectionParameter : INormalizable { /// /// **Fields** selection mode (optional, i.e. `select.fields=balance` is the same as `select=balance`). \ @@ -31,6 +31,11 @@ public class SelectionParameter /// [JsonSchemaType(typeof(List))] public List Values { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } public class SelectionField diff --git a/Tzkt.Api/Parameters/SortParameter.cs b/Tzkt.Api/Parameters/SortParameter.cs index f7d5bf479..11c9ec280 100644 --- a/Tzkt.Api/Parameters/SortParameter.cs +++ b/Tzkt.Api/Parameters/SortParameter.cs @@ -5,7 +5,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(SortBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class SortParameter + public class SortParameter : INormalizable { /// /// **Ascending** sort mode (optional, i.e. `sort.asc=id` is the same as `sort=id`). \ @@ -45,5 +45,10 @@ public bool Validate(params string[] fields) return true; } + + public string Normalize(string name) + { + return Asc != null ? $"sort.asc={Asc}&" : $"sort.desc={Desc}&"; + } } } diff --git a/Tzkt.Api/Parameters/StringParameter.cs b/Tzkt.Api/Parameters/StringParameter.cs index ce65da430..c58329944 100644 --- a/Tzkt.Api/Parameters/StringParameter.cs +++ b/Tzkt.Api/Parameters/StringParameter.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using System.Text; using Microsoft.AspNetCore.Mvc; using NJsonSchema.Annotations; @@ -6,7 +8,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(StringBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class StringParameter + public class StringParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -67,5 +69,48 @@ public class StringParameter /// Example: `?parameters.null` or `?parameters.null=false`. /// public bool? Null { get; set; } + + + public string Normalize(string name) + { + var sb = new StringBuilder(); + + if (Eq != null) + { + sb.Append($"{name}.eq={Eq}&"); + } + + if (Ne != null) + { + sb.Append($"{name}.ne={Ne}&"); + } + + if (As != null) + { + sb.Append($"{name}.as={As}&"); + } + + if (Un != null) + { + sb.Append($"{name}.un={Un}&"); + } + + if (In != null && In.Any()) + { + sb.Append($"{name}.in={string.Join(",", In.OrderBy(x => x))}&"); + } + + if (Ni != null && Ni.Any()) + { + sb.Append($"{name}.ni={string.Join(",", Ni.OrderBy(x => x))}&"); + } + + if (Null != null) + { + sb.Append($"{name}.null={Null}&"); + } + + return sb.ToString(); + } } } diff --git a/Tzkt.Api/Parameters/TimestampParameter.cs b/Tzkt.Api/Parameters/TimestampParameter.cs index f2f4c284c..9b1534429 100644 --- a/Tzkt.Api/Parameters/TimestampParameter.cs +++ b/Tzkt.Api/Parameters/TimestampParameter.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; using Microsoft.AspNetCore.Mvc; using NJsonSchema.Annotations; using Tzkt.Api.Services.Cache; @@ -8,7 +10,7 @@ namespace Tzkt.Api { [ModelBinder(BinderType = typeof(TimestampBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] - public class TimestampParameter + public class TimestampParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -104,5 +106,52 @@ public static Int32Parameter FromDateTimeParameter(DateTimeParameter timestamp, return res; } #endregion + + public string Normalize(string name) + { + var sb = new StringBuilder(); + + if (Eq != null) + { + sb.Append($"{name}.eq={Eq}&"); + } + + if (Ne != null) + { + sb.Append($"{name}.ne={Ne}&"); + } + + if (Gt != null) + { + sb.Append($"{name}.gt={Gt}&"); + } + + if (Ge != null) + { + sb.Append($"{name}.ge={Ge}&"); + } + + if (Lt != null) + { + sb.Append($"{name}.lt={Lt}&"); + } + + if (Le != null) + { + sb.Append($"{name}.le={Le}&"); + } + + if (In != null && In.Any()) + { + sb.Append($"{name}.in={string.Join(",", In.OrderBy(x => x))}&"); + } + + if (Ni != null && Ni.Any()) + { + sb.Append($"{name}.ni={string.Join(",", Ni.OrderBy(x => x))}&"); + } + + return sb.ToString(); + } } } diff --git a/Tzkt.Api/Parameters/TokenBalanceFilter.cs b/Tzkt.Api/Parameters/TokenBalanceFilter.cs index 48f45b228..da62db655 100644 --- a/Tzkt.Api/Parameters/TokenBalanceFilter.cs +++ b/Tzkt.Api/Parameters/TokenBalanceFilter.cs @@ -1,6 +1,6 @@ namespace Tzkt.Api { - public class TokenBalanceFilter + public class TokenBalanceFilter : INormalizable { /// /// Filter by internal TzKT id. @@ -49,5 +49,10 @@ public class TokenBalanceFilter /// Click on the parameter to expand more details. /// public TimestampParameter lastTime { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/TokenBalanceShortFilter.cs b/Tzkt.Api/Parameters/TokenBalanceShortFilter.cs index 5b6162287..53cfea1ce 100644 --- a/Tzkt.Api/Parameters/TokenBalanceShortFilter.cs +++ b/Tzkt.Api/Parameters/TokenBalanceShortFilter.cs @@ -1,6 +1,6 @@ namespace Tzkt.Api { - public class TokenBalanceShortFilter + public class TokenBalanceShortFilter : INormalizable { /// /// Filter by account address. @@ -19,5 +19,10 @@ public class TokenBalanceShortFilter /// Click on the parameter to expand more details. /// public NatParameter balance { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/TokenFilter.cs b/Tzkt.Api/Parameters/TokenFilter.cs index d32c45f4a..0b0a0f174 100644 --- a/Tzkt.Api/Parameters/TokenFilter.cs +++ b/Tzkt.Api/Parameters/TokenFilter.cs @@ -1,6 +1,6 @@ namespace Tzkt.Api { - public class TokenFilter + public class TokenFilter : INormalizable { /// /// Filter by internal TzKT id. Note, this is not the same as `tokenId` nat value. @@ -57,5 +57,10 @@ public class TokenFilter /// Click on the parameter to expand more details. /// public JsonParameter metadata { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/TokenInfoFilter.cs b/Tzkt.Api/Parameters/TokenInfoFilter.cs index 8fab7d9dd..6e0f84da2 100644 --- a/Tzkt.Api/Parameters/TokenInfoFilter.cs +++ b/Tzkt.Api/Parameters/TokenInfoFilter.cs @@ -2,7 +2,7 @@ namespace Tzkt.Api { - public class TokenInfoFilter + public class TokenInfoFilter : INormalizable { /// /// Filter by internal TzKT id. Note, this is not the same as `tokenId`. @@ -37,5 +37,10 @@ public class TokenInfoFilter [JsonIgnore] public bool HasFilters => contract != null || tokenId != null || standard != null; + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/TokenStandardParameter.cs b/Tzkt.Api/Parameters/TokenStandardParameter.cs index 449b3ba77..c8fe33438 100644 --- a/Tzkt.Api/Parameters/TokenStandardParameter.cs +++ b/Tzkt.Api/Parameters/TokenStandardParameter.cs @@ -6,7 +6,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(TokenStandardBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "fa1.2,fa2")] - public class TokenStandardParameter + public class TokenStandardParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -25,5 +25,10 @@ public class TokenStandardParameter /// [JsonSchemaType(typeof(string))] public int? Ne { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/TokenTransferFilter.cs b/Tzkt.Api/Parameters/TokenTransferFilter.cs index 9eee80b60..65c74a442 100644 --- a/Tzkt.Api/Parameters/TokenTransferFilter.cs +++ b/Tzkt.Api/Parameters/TokenTransferFilter.cs @@ -1,6 +1,6 @@ namespace Tzkt.Api { - public class TokenTransferFilter + public class TokenTransferFilter : INormalizable { /// /// Filter by internal TzKT id. @@ -69,5 +69,10 @@ public class TokenTransferFilter /// Click on the parameter to expand more details. /// public Int32NullParameter migrationId { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/VoteParameter.cs b/Tzkt.Api/Parameters/VoteParameter.cs index eefe8cbc7..74dabc3a1 100644 --- a/Tzkt.Api/Parameters/VoteParameter.cs +++ b/Tzkt.Api/Parameters/VoteParameter.cs @@ -7,7 +7,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(VoteBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "yay,nay,pass")] - public class VoteParameter + public class VoteParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -44,5 +44,10 @@ public class VoteParameter /// [JsonSchemaType(typeof(List))] public List Ni { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Parameters/VoterStatusParameter.cs b/Tzkt.Api/Parameters/VoterStatusParameter.cs index c4be1004e..4258c50eb 100644 --- a/Tzkt.Api/Parameters/VoterStatusParameter.cs +++ b/Tzkt.Api/Parameters/VoterStatusParameter.cs @@ -7,7 +7,7 @@ namespace Tzkt.Api [ModelBinder(BinderType = typeof(VoterStatusBinder))] [JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")] [JsonSchemaExtensionData("x-tzkt-query-parameter", "none,upvoted,voted_yay,voted_nay,voted_pass")] - public class VoterStatusParameter + public class VoterStatusParameter : INormalizable { /// /// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \ @@ -44,5 +44,10 @@ public class VoterStatusParameter /// [JsonSchemaType(typeof(List))] public List Ni { get; set; } + + public string Normalize(string name) + { + throw new System.NotImplementedException(); + } } } diff --git a/Tzkt.Api/Services/Output/OutputCacheConfig.cs b/Tzkt.Api/Services/Output/OutputCacheConfig.cs new file mode 100644 index 000000000..8e2fff872 --- /dev/null +++ b/Tzkt.Api/Services/Output/OutputCacheConfig.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; + +namespace Tzkt.Api.Services +{ + public class OutputCacheConfig + { + public int CacheSize { get; set; } = 500; + public int CompressionLimit { get; set; } = 1000; + } + + public static class OutputCacheConfigExt + { + public static OutputCacheConfig GetOutputCacheConfig(this IConfiguration config) + { + return config.GetSection("OutputCache")?.Get() ?? new OutputCacheConfig(); + } + } +} diff --git a/Tzkt.Api/Services/Output/OutputCacheEntity.cs b/Tzkt.Api/Services/Output/OutputCacheEntity.cs new file mode 100644 index 000000000..c0e34aa77 --- /dev/null +++ b/Tzkt.Api/Services/Output/OutputCacheEntity.cs @@ -0,0 +1,11 @@ +using System; + +namespace Tzkt.Api.Services.Output +{ + public class OutputCacheEntity + { + public DateTime LastAccess { get; set; } + public byte[] Bytes { get; init; } + public bool IsCompressed { get; init; } + } +} \ No newline at end of file diff --git a/Tzkt.Api/Services/Output/OutputCacheKeysProvider.cs b/Tzkt.Api/Services/Output/OutputCacheKeysProvider.cs new file mode 100644 index 000000000..06642efb4 --- /dev/null +++ b/Tzkt.Api/Services/Output/OutputCacheKeysProvider.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Tzkt.Api.Services.Output +{ + public class OutputCacheKeysProvider + { + public static string BuildQuery(string requestPath, params (string, object)[] args) + { + var sb = new StringBuilder(requestPath); + sb.Append('?'); + foreach (var (name, value) in args) + { + //TODO Check for null + if (value == null) + continue; + + sb.Append(value is INormalizable normalizable ? normalizable.Normalize(name) : $"{name}={value}&"); + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/Tzkt.Api/Services/Output/OutputCachingExtension.cs b/Tzkt.Api/Services/Output/OutputCachingExtension.cs new file mode 100644 index 000000000..fc967b547 --- /dev/null +++ b/Tzkt.Api/Services/Output/OutputCachingExtension.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Tzkt.Api.Services.Output +{ + public static class OutputCachingExtension + { + public static void AddOutputCaching(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + } + } +} \ No newline at end of file diff --git a/Tzkt.Api/Services/Output/OutputCachingService.cs b/Tzkt.Api/Services/Output/OutputCachingService.cs new file mode 100644 index 000000000..fee2739d7 --- /dev/null +++ b/Tzkt.Api/Services/Output/OutputCachingService.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Tzkt.Api.Services.Output +{ + public class OutputCachingService + { + readonly ILogger Logger; + + readonly long CacheSizeLimit; + readonly long CompressionLimit; + readonly Dictionary Cache; + + long CacheUsed = 0; + + public OutputCachingService(IConfiguration configuration, ILogger logger) + { + Logger = logger; + CacheSizeLimit = configuration.GetOutputCacheConfig().CacheSize * 1_000_000; + CompressionLimit = configuration.GetOutputCacheConfig().CompressionLimit; + Cache = new Dictionary(); + } + + public bool TryGetFromCache(HttpContext context, string key, out OutputCacheEntity response) + { + OutputCacheEntity cacheEntity; + lock (Cache) + { + if (!Cache.TryGetValue(key, out cacheEntity)) + { + response = null; + return false; + } + } + + lock (cacheEntity) + { + cacheEntity.LastAccess = DateTime.UtcNow; + + if (!cacheEntity.IsCompressed) + { + response = cacheEntity; + return true; + } + + var acceptEncodingHeaders = context.Request.Headers["accept-encoding"].ToString(); + + if (!string.IsNullOrEmpty(acceptEncodingHeaders) && acceptEncodingHeaders.Contains("gzip")) + { + context.Response.Headers.Add("Content-encoding", "gzip"); + response = cacheEntity; + return true; + } + + using (var memoryStream = new MemoryStream(cacheEntity.Bytes)) + using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + using (var memoryStreamOutput = new MemoryStream()) + { + gZipStream.CopyTo(memoryStreamOutput); + response = new OutputCacheEntity + { + Bytes = memoryStreamOutput.ToArray(), + LastAccess = DateTime.UtcNow, + IsCompressed = false + }; + } + + return true; + } + } + + public void Set(string key, object res) + { + var buffer = JsonSerializer.SerializeToUtf8Bytes(res); + byte[] bytesToBeCached; + var compressed = false; + if (buffer.Length < CompressionLimit) + { + bytesToBeCached = buffer; + } + else + { + using (var outStream = new MemoryStream()) + { + using (var zipStream = new GZipStream(outStream, CompressionMode.Compress)) + using (var mStream = new MemoryStream(buffer)) + mStream.CopyTo(zipStream); + + bytesToBeCached = outStream.ToArray(); + compressed = true; + } + } + + if (bytesToBeCached.Length > CacheSizeLimit) + { + Logger.LogWarning("{Key} too big to be cached. Cache size: {CacheSizeLimit} bytes. Response size: {BytesToBeCached} bytes", key, CacheSizeLimit, bytesToBeCached.Length); + return; + } + + lock (Cache) + { + if (Cache.Any() && CacheUsed + bytesToBeCached.Length >= CacheSizeLimit) + { + var toDelete = Cache.OrderBy(x => x.Value.LastAccess).Select(x => x.Key).ToList(); + + while (Cache.Any() && CacheUsed + bytesToBeCached.Length >= CacheSizeLimit) + { + var keyToDelete = toDelete.FirstOrDefault(); + CacheUsed -= Cache[keyToDelete].Bytes.Length; + Cache.Remove(keyToDelete); + toDelete.RemoveAt(0); + } + } + + CacheUsed += bytesToBeCached.Length; + Cache[key] = new OutputCacheEntity + { + Bytes = bytesToBeCached, + LastAccess = DateTime.UtcNow, + IsCompressed = compressed + }; + } + } + + public void Invalidate() + { + lock (Cache) + { + Cache.Clear(); + CacheUsed = 0; + } + } + } +} \ No newline at end of file diff --git a/Tzkt.Api/Services/Sync/StateListener.cs b/Tzkt.Api/Services/Sync/StateListener.cs index b294bdc25..0203cc4d6 100644 --- a/Tzkt.Api/Services/Sync/StateListener.cs +++ b/Tzkt.Api/Services/Sync/StateListener.cs @@ -11,6 +11,7 @@ using Dapper; using Npgsql; using Tzkt.Api.Services.Cache; +using Tzkt.Api.Services.Output; using Tzkt.Api.Websocket; namespace Tzkt.Api.Services.Sync @@ -39,6 +40,7 @@ public class StateListener : BackgroundService readonly SoftwareCache Software; readonly QuotesCache Quotes; readonly TimeCache Times; + readonly OutputCachingService OutputCache; readonly HomeService Home; readonly IEnumerable Processors; readonly ILogger Logger; @@ -55,6 +57,7 @@ public StateListener( ProtocolsCache protocols, QuotesCache quotes, TimeCache times, + OutputCachingService outputCache, HomeService home, IEnumerable processors, IConfiguration config, @@ -70,6 +73,7 @@ public StateListener( Software = software; Quotes = quotes; Times = times; + OutputCache = outputCache; Home = home; Processors = processors; Logger = logger; @@ -217,6 +221,7 @@ async Task NotifyStateAsync() await Protocols.UpdateAsync(); await Quotes.UpdateAsync(); await Times.UpdateAsync(); + OutputCache.Invalidate(); #endregion #region send events diff --git a/Tzkt.Api/Startup.cs b/Tzkt.Api/Startup.cs index e04f59b1e..047cefc48 100644 --- a/Tzkt.Api/Startup.cs +++ b/Tzkt.Api/Startup.cs @@ -12,6 +12,7 @@ using Tzkt.Api.Services; using Tzkt.Api.Services.Auth; using Tzkt.Api.Services.Cache; +using Tzkt.Api.Services.Output; using Tzkt.Api.Services.Sync; using Tzkt.Api.Swagger; using Tzkt.Api.Websocket; @@ -42,6 +43,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); diff --git a/Tzkt.Api/appsettings.json b/Tzkt.Api/appsettings.json index f65a6c329..373964355 100644 --- a/Tzkt.Api/appsettings.json +++ b/Tzkt.Api/appsettings.json @@ -13,7 +13,7 @@ "MaxTokenTransfersSubscriptions": 50 }, "ConnectionStrings": { - "DefaultConnection": "host=db;port=5432;database=tzkt_db;username=tzkt;password=qwerty;command timeout=600;" + "DefaultConnection": "host=localhost;port=5433;database=tzkt_db;username=tzkt;password=qwerty;command timeout=600;" }, "Home": { "Enabled": false, diff --git a/docker-compose.ithaca.yml b/docker-compose.ithaca.yml index 1d95a7bc1..53bfe3466 100644 --- a/docker-compose.ithaca.yml +++ b/docker-compose.ithaca.yml @@ -10,7 +10,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-qwerty} POSTGRES_DB: ${POSTGRES_DB:-tzkt_db} volumes: - - postgres:/var/lib/postgresql/data + - ithaca_postgres:/var/lib/postgresql/data ports: - 127.0.0.1:5433:5432 @@ -37,4 +37,4 @@ services: - ithaca-db volumes: - postgres: + ithaca_postgres: