diff --git a/build/generate-client.sh b/build/generate-client.sh index 30c64780db..ce59fcb363 100644 --- a/build/generate-client.sh +++ b/build/generate-client.sh @@ -1,7 +1,7 @@ curl -X POST https://generator3.swagger.io/api/generate \ -H 'content-type: application/json' \ -d '{ - "specURL" : "https://collector.exceptionless.io/docs/v2/swagger.json", + "specURL" : "https://collector.exceptionless.io/docs/v2/openapi.json", "lang" : "typescript-fetch", "type" : "CLIENT", "options" : { @@ -11,4 +11,4 @@ curl -X POST https://generator3.swagger.io/api/generate \ } }, "codegenVersion" : "V3" - }' --output exceptionless-ts.zip \ No newline at end of file + }' --output exceptionless-ts.zip diff --git a/src/Exceptionless.Core/Configuration/AppOptions.cs b/src/Exceptionless.Core/Configuration/AppOptions.cs index 02de4342ef..19f94e37ac 100644 --- a/src/Exceptionless.Core/Configuration/AppOptions.cs +++ b/src/Exceptionless.Core/Configuration/AppOptions.cs @@ -1,9 +1,8 @@ using System.Diagnostics; +using System.Text.Json.Serialization; using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace Exceptionless.Core; @@ -26,7 +25,7 @@ public class AppOptions /// public string? ExceptionlessServerUrl { get; internal set; } - [JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public AppMode AppMode { get; internal set; } public string AppScope { get; internal set; } = null!; diff --git a/src/Exceptionless.Core/Configuration/EmailOptions.cs b/src/Exceptionless.Core/Configuration/EmailOptions.cs index 17d560d967..0fe9fa69ef 100644 --- a/src/Exceptionless.Core/Configuration/EmailOptions.cs +++ b/src/Exceptionless.Core/Configuration/EmailOptions.cs @@ -1,8 +1,7 @@ -using Exceptionless.Core.Extensions; +using System.Text.Json.Serialization; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Utility; using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace Exceptionless.Core.Configuration; @@ -26,7 +25,7 @@ public class EmailOptions public int SmtpPort { get; internal set; } - [JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public SmtpEncryption SmtpEncryption { get; internal set; } public string? SmtpUser { get; internal set; } diff --git a/src/Exceptionless.Core/Models/Stack.cs b/src/Exceptionless.Core/Models/Stack.cs index 364e9c269f..f387a2f1b2 100644 --- a/src/Exceptionless.Core/Models/Stack.cs +++ b/src/Exceptionless.Core/Models/Stack.cs @@ -1,8 +1,8 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Foundatio.Repositories.Models; -using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace Exceptionless.Core.Models; @@ -123,13 +123,26 @@ public static class KnownTypes } } -[JsonConverter(typeof(StringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] +[Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))] public enum StackStatus { - [EnumMember(Value = "open")] Open, - [EnumMember(Value = "fixed")] Fixed, - [EnumMember(Value = "regressed")] Regressed, - [EnumMember(Value = "snoozed")] Snoozed, - [EnumMember(Value = "ignored")] Ignored, - [EnumMember(Value = "discarded")] Discarded + [JsonStringEnumMemberName("open")] + [EnumMember(Value = "open")] + Open, + [JsonStringEnumMemberName("fixed")] + [EnumMember(Value = "fixed")] + Fixed, + [JsonStringEnumMemberName("regressed")] + [EnumMember(Value = "regressed")] + Regressed, + [JsonStringEnumMemberName("snoozed")] + [EnumMember(Value = "snoozed")] + Snoozed, + [JsonStringEnumMemberName("ignored")] + [EnumMember(Value = "ignored")] + Ignored, + [JsonStringEnumMemberName("discarded")] + [EnumMember(Value = "discarded")] + Discarded } diff --git a/src/Exceptionless.Web/ClientApp/package.json b/src/Exceptionless.Web/ClientApp/package.json index cf2a6dafe7..bc5d72efa7 100644 --- a/src/Exceptionless.Web/ClientApp/package.json +++ b/src/Exceptionless.Web/ClientApp/package.json @@ -15,7 +15,7 @@ "format": "npm run format:prettier && npm run format:eslint", "format:eslint": "eslint . --fix --concurrency=auto", "format:prettier": "prettier --write .", - "generate-models": "swagger-typescript-api generate -p http://localhost:5200/docs/v2/swagger.json -o ./src/lib/generated -n api.ts --no-client --templates api-templates", + "generate-models": "swagger-typescript-api generate -p http://localhost:5200/docs/v2/openapi.json -o ./src/lib/generated -n api.ts --no-client --templates api-templates", "generate-templates": "swagger-typescript-api generate-templates -o api-templates", "test:e2e": "playwright test", "test:unit": "vitest run", diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index 08713d9da3..f62b1e3275 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -18,6 +18,7 @@ using Exceptionless.Web.Extensions; using Exceptionless.Web.Models; using Exceptionless.Web.Utility; +using Exceptionless.Web.Utility.OpenApi; using FluentValidation; using Foundatio.Caching; using Foundatio.Queues; diff --git a/src/Exceptionless.Web/Controllers/StackController.cs b/src/Exceptionless.Web/Controllers/StackController.cs index 1a0a9021c7..986d742197 100644 --- a/src/Exceptionless.Web/Controllers/StackController.cs +++ b/src/Exceptionless.Web/Controllers/StackController.cs @@ -21,7 +21,7 @@ using McSherry.SemanticVersioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json.Linq; +using System.Text.Json; namespace Exceptionless.Web.Controllers; @@ -132,14 +132,14 @@ public async Task MarkFixedAsync(string ids, string? version = nul [HttpPost("mark-fixed")] [Consumes("application/json")] [ApiExplorerSettings(IgnoreApi = true)] - public async Task MarkFixedAsync(JObject data) + public async Task MarkFixedAsync(JsonDocument data) { string? id = null; - if (data.TryGetValue("ErrorStack", out var value)) - id = value.Value(); + if (data.RootElement.TryGetProperty("ErrorStack", out var errorStackProp)) + id = errorStackProp.GetString(); - if (data.TryGetValue("Stack", out value)) - id = value.Value(); + if (data.RootElement.TryGetProperty("Stack", out var stackProp)) + id = stackProp.GetString(); if (String.IsNullOrEmpty(id)) return NotFound(); @@ -217,14 +217,14 @@ public async Task AddLinkAsync(string id, ValueFromBody [HttpPost("add-link")] [Consumes("application/json")] [ApiExplorerSettings(IgnoreApi = true)] - public async Task AddLinkAsync(JObject data) + public async Task AddLinkAsync(JsonDocument data) { string? id = null; - if (data.TryGetValue("ErrorStack", out var value)) - id = value.Value(); + if (data.RootElement.TryGetProperty("ErrorStack", out var errorStackProp)) + id = errorStackProp.GetString(); - if (data.TryGetValue("Stack", out value)) - id = value.Value(); + if (data.RootElement.TryGetProperty("Stack", out var stackProp)) + id = stackProp.GetString(); if (String.IsNullOrEmpty(id)) return NotFound(); @@ -232,7 +232,7 @@ public async Task AddLinkAsync(JObject data) if (id.StartsWith("http")) id = id.Substring(id.LastIndexOf('/') + 1); - string? url = data.GetValue("Link")?.Value(); + string? url = data.RootElement.TryGetProperty("Link", out var linkProp) ? linkProp.GetString() : null; return await AddLinkAsync(id, new ValueFromBody(url)); } diff --git a/src/Exceptionless.Web/Controllers/WebHookController.cs b/src/Exceptionless.Web/Controllers/WebHookController.cs index 72d2e9170b..1a347334e8 100644 --- a/src/Exceptionless.Web/Controllers/WebHookController.cs +++ b/src/Exceptionless.Web/Controllers/WebHookController.cs @@ -11,7 +11,7 @@ using Foundatio.Repositories; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json.Linq; +using System.Text.Json; namespace Exceptionless.App.Controllers.API; @@ -105,10 +105,10 @@ public Task> DeleteAsync(string ids) [HttpPost("~/api/v1/projecthook/subscribe")] [Consumes("application/json")] [ApiExplorerSettings(IgnoreApi = true)] - public async Task> SubscribeAsync(JObject data, int apiVersion = 1) + public async Task> SubscribeAsync(JsonDocument data, int apiVersion = 1) { - string? eventType = data.GetValue("event")?.Value(); - string? url = data.GetValue("target_url")?.Value(); + string? eventType = data.RootElement.TryGetProperty("event", out var eventProp) ? eventProp.GetString() : null; + string? url = data.RootElement.TryGetProperty("target_url", out var urlProp) ? urlProp.GetString() : null; if (String.IsNullOrEmpty(eventType) || String.IsNullOrEmpty(url)) return BadRequest(); @@ -143,9 +143,9 @@ public async Task> SubscribeAsync(JObject data, int apiVer [HttpPost("~/api/v1/projecthook/unsubscribe")] [Consumes("application/json")] [ApiExplorerSettings(IgnoreApi = true)] - public async Task UnsubscribeAsync(JObject data) + public async Task UnsubscribeAsync(JsonDocument data) { - string? targetUrl = data.GetValue("target_url")?.Value(); + string? targetUrl = data.RootElement.TryGetProperty("target_url", out var urlProp) ? urlProp.GetString() : null; // don't let this anon method delete non-zapier hooks if (targetUrl is null || !targetUrl.StartsWith("https://hooks.zapier.com")) diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index c53397f1d6..bda8c36a0f 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -16,10 +16,11 @@ + - + @@ -34,7 +35,6 @@ - diff --git a/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs b/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs index 75f5ae118f..f30a71235b 100644 --- a/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs +++ b/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs @@ -1,8 +1,9 @@ using System.Collections.Concurrent; using System.Net.WebSockets; using System.Text; +using System.Text.Json; using Exceptionless.Core; -using Newtonsoft.Json; +using Exceptionless.Web.Utility; namespace Exceptionless.Web.Hubs; @@ -11,12 +12,15 @@ public class WebSocketConnectionManager : IDisposable private static readonly ArraySegment _keepAliveMessage = new(Encoding.ASCII.GetBytes("{}"), 0, 2); private readonly ConcurrentDictionary _connections = new(); private readonly Timer? _timer; - private readonly JsonSerializerSettings _serializerSettings; + private readonly JsonSerializerOptions _serializerOptions; private readonly ILogger _logger; - public WebSocketConnectionManager(AppOptions options, JsonSerializerSettings serializerSettings, ILoggerFactory loggerFactory) + public WebSocketConnectionManager(AppOptions options, ILoggerFactory loggerFactory) { - _serializerSettings = serializerSettings; + _serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = LowerCaseUnderscoreNamingPolicy.Instance + }; _logger = loggerFactory.CreateLogger(); if (!options.EnableWebSockets) return; @@ -119,7 +123,7 @@ private Task SendMessageAsync(WebSocket socket, object message) if (!CanSendWebSocketMessage(socket)) return Task.CompletedTask; - string serializedMessage = JsonConvert.SerializeObject(message, _serializerSettings); + string serializedMessage = JsonSerializer.Serialize(message, _serializerOptions); Task.Factory.StartNew(async () => { if (!CanSendWebSocketMessage(socket)) diff --git a/src/Exceptionless.Web/Models/Auth/ExternalAuthInfo.cs b/src/Exceptionless.Web/Models/Auth/ExternalAuthInfo.cs index b23d1b2acf..ea39e35fc4 100644 --- a/src/Exceptionless.Web/Models/Auth/ExternalAuthInfo.cs +++ b/src/Exceptionless.Web/Models/Auth/ExternalAuthInfo.cs @@ -1,21 +1,23 @@ using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; +using System.Text.Json.Serialization; namespace Exceptionless.Web.Models; // NOTE: This will bypass our LowerCaseUnderscorePropertyNamesContractResolver and provide the correct casing. -[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] public record ExternalAuthInfo { [Required] + [JsonPropertyName("clientId")] public required string ClientId { get; init; } [Required] + [JsonPropertyName("code")] public required string Code { get; init; } [Required] + [JsonPropertyName("redirectUri")] public required string RedirectUri { get; init; } + [JsonPropertyName("inviteToken")] public string? InviteToken { get; init; } } diff --git a/src/Exceptionless.Web/Startup.cs b/src/Exceptionless.Web/Startup.cs index dc70c88c24..9e1f241f61 100644 --- a/src/Exceptionless.Web/Startup.cs +++ b/src/Exceptionless.Web/Startup.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Security.Claims; +using System.Text.Json; using Exceptionless.Core; using Exceptionless.Core.Authorization; using Exceptionless.Core.Extensions; @@ -7,9 +8,11 @@ using Exceptionless.Core.Validation; using Exceptionless.Web.Extensions; using Exceptionless.Web.Hubs; +using Exceptionless.Web.Models; using Exceptionless.Web.Security; using Exceptionless.Web.Utility; using Exceptionless.Web.Utility.Handlers; +using Exceptionless.Web.Utility.OpenApi; using FluentValidation; using Foundatio.Extensions.Hosting.Startup; using Foundatio.Repositories.Exceptions; @@ -20,10 +23,11 @@ using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.NewtonsoftJson; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.OpenApi; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi; -using Newtonsoft.Json; +using Scalar.AspNetCore; using Serilog; using Serilog.Events; @@ -59,15 +63,27 @@ public void ConfigureServices(IServiceCollection services) services.AddControllers(o => { o.ModelBinderProviders.Insert(0, new CustomAttributesModelBinderProvider()); - o.ModelMetadataDetailsProviders.Add(new NewtonsoftJsonValidationMetadataProvider(new ExceptionlessNamingStrategy())); + o.ModelMetadataDetailsProviders.Add(new SystemTextJsonValidationMetadataProvider(LowerCaseUnderscoreNamingPolicy.Instance)); o.InputFormatters.Insert(0, new RawRequestBodyFormatter()); }) - .AddNewtonsoftJson(o => + .AddJsonOptions(o => { - o.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include; - o.SerializerSettings.NullValueHandling = NullValueHandling.Include; - o.SerializerSettings.Formatting = Formatting.Indented; - o.SerializerSettings.ContractResolver = Core.Bootstrapper.GetJsonContractResolver(); // TODO: See if we can resolve this from the di. + o.JsonSerializerOptions.PropertyNamingPolicy = LowerCaseUnderscoreNamingPolicy.Instance; + o.JsonSerializerOptions.Converters.Add(new DeltaJsonConverterFactory()); +#if DEBUG + o.JsonSerializerOptions.RespectNullableAnnotations = true; +#endif + }); + + // Have to add this to get the open api json file to be snake case. + services.ConfigureHttpJsonOptions(o => + { + o.SerializerOptions.PropertyNamingPolicy = LowerCaseUnderscoreNamingPolicy.Instance; + o.SerializerOptions.Converters.Add(new DeltaJsonConverterFactory()); + //o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); +#if DEBUG + o.SerializerOptions.RespectNullableAnnotations = true; +#endif }); services.AddProblemDetails(o => o.CustomizeProblemDetails = CustomizeProblemDetails); @@ -94,65 +110,30 @@ public void ConfigureServices(IServiceCollection services) r.ConstraintMap.Add("tokens", typeof(TokensRouteConstraint)); }); - services.AddSwaggerGen(c => + services.AddOpenApi(o => { - c.SwaggerDoc("v2", new OpenApiInfo + // Customize schema names to strip "DeltaOf" prefix (e.g., DeltaOfUpdateToken -> UpdateToken) + o.CreateSchemaReferenceId = typeInfo => { - Title = "Exceptionless API", - Version = "v2", - TermsOfService = new Uri("https://exceptionless.com/terms/"), - Contact = new OpenApiContact + var type = typeInfo.Type; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Delta<>)) { - Name = "Exceptionless", - Email = String.Empty, - Url = new Uri("https://github.com/exceptionless/Exceptionless") - }, - License = new OpenApiLicense - { - Name = "Apache License 2.0", - Url = new Uri("https://github.com/exceptionless/Exceptionless/blob/main/LICENSE.txt") + var innerType = type.GetGenericArguments()[0]; + return innerType.Name; } - }); - - c.AddSecurityDefinition("Basic", new OpenApiSecurityScheme - { - Description = "Basic HTTP Authentication", - Scheme = "basic", - Type = SecuritySchemeType.Http - }); - c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - Description = "Authorization token. Example: \"Bearer {apikey}\"", - Scheme = "bearer", - Type = SecuritySchemeType.Http - }); - c.AddSecurityDefinition("Token", new OpenApiSecurityScheme - { - Description = "Authorization token. Example: \"Bearer {apikey}\"", - Name = "access_token", - In = ParameterLocation.Query, - Type = SecuritySchemeType.ApiKey - }); - c.AddSecurityRequirement(document => new OpenApiSecurityRequirement - { - { new OpenApiSecuritySchemeReference("Basic", document), [] }, - { new OpenApiSecuritySchemeReference("Bearer", document), [] }, - { new OpenApiSecuritySchemeReference("Token", document), [] } - }); - - string xmlDocPath = Path.Combine(AppContext.BaseDirectory, "Exceptionless.Web.xml"); - if (File.Exists(xmlDocPath)) - c.IncludeXmlComments(xmlDocPath); - - c.IgnoreObsoleteActions(); - c.OperationFilter(); - c.SchemaFilter(); - c.DocumentFilter(); + return OpenApiOptions.CreateDefaultSchemaReferenceId(typeInfo); + }; - c.SupportNonNullableReferenceTypes(); + o.AddDocumentTransformer(); + o.AddDocumentTransformer(); + o.AddOperationTransformer(); + o.AddOperationTransformer(); + o.AddSchemaTransformer(); + o.AddSchemaTransformer(); + o.AddSchemaTransformer(); + o.AddSchemaTransformer(); }); - services.AddSwaggerGenNewtonsoftSupport(); var appOptions = AppOptions.ReadFromConfiguration(Configuration); Bootstrapper.RegisterServices(services, appOptions, Log.Logger.ToLoggerFactory()); @@ -213,7 +194,6 @@ ApplicationException applicationException when applicationException.Message.Cont } }); app.UseStatusCodePages(); - app.UseMiddleware(); app.UseOpenTelemetryPrometheusScrapingEndpoint(); @@ -339,14 +319,6 @@ ApplicationException applicationException when applicationException.Message.Cont // Reject event posts in organizations over their max event limits. app.UseMiddleware(); - app.UseSwagger(c => c.RouteTemplate = "docs/{documentName}/swagger.json"); - app.UseSwaggerUI(s => - { - s.RoutePrefix = "docs"; - s.SwaggerEndpoint("/docs/v2/swagger.json", "Exceptionless API"); - s.InjectStylesheet("/docs.css"); - }); - if (options.EnableWebSockets) { app.UseWebSockets(); @@ -355,6 +327,27 @@ ApplicationException applicationException when applicationException.Message.Cont app.UseEndpoints(endpoints => { + endpoints.MapOpenApi("/docs/v2/openapi.json"); + endpoints.MapScalarApiReference("docs", o => + { + o.WithTitle("Exceptionless API") + .WithTheme(ScalarTheme.Default) + .AddHttpAuthentication("BasicAuth", auth => + { + auth.Username = "your-username"; + auth.Password = "your-password"; + }) + .AddHttpAuthentication("BearerAuth", auth => + { + auth.Token = "apikey"; + }) + .AddApiKeyAuthentication("ApiKey", apiKey => + { + apiKey.Value = "access_token"; + }) + .AddPreferredSecuritySchemes("BearerAuth"); + }); + endpoints.MapControllers(); endpoints.MapFallback("{**slug:nonfile}", CreateRequestDelegate(endpoints, "/index.html")); }); diff --git a/src/Exceptionless.Web/Utility/AutoValidationActionFilter.cs b/src/Exceptionless.Web/Utility/AutoValidationActionFilter.cs index 896bca5905..9e03dd3af7 100644 --- a/src/Exceptionless.Web/Utility/AutoValidationActionFilter.cs +++ b/src/Exceptionless.Web/Utility/AutoValidationActionFilter.cs @@ -1,6 +1,7 @@ using System.Dynamic; using System.IO.Pipelines; using System.Security.Claims; +using System.Text.Json; using Exceptionless.Core.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -33,7 +34,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE continue; // We don't support validating JSON Types - if (subject is Newtonsoft.Json.Linq.JToken or DynamicObject) + if (subject is JsonDocument or JsonElement or DynamicObject) continue; (bool isValid, var errors) = await MiniValidator.TryValidateAsync(subject, _serviceProvider, recurse: true); @@ -43,7 +44,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE foreach (var error in errors) { // TODO: Verify nested object keys - // NOTE: Fallback to finding model state errors where the serializer already changed the key, but differs from ModelState like ExternalAuthInfo (without NamingStrategyType) + // NOTE: Fallback to finding model state errors where the serializer already changed the key, but differs from ModelState like ExternalAuthInfo (without NamingStrategyType) var modelStateEntry = context.ModelState[error.Key] ?? context.ModelState[error.Key.ToLowerUnderscoredWords()]; foreach (string errorMessage in error.Value) { diff --git a/src/Exceptionless.Web/Utility/Delta/Delta.cs b/src/Exceptionless.Web/Utility/Delta/Delta.cs index 727d2d955f..042cf3c739 100644 --- a/src/Exceptionless.Web/Utility/Delta/Delta.cs +++ b/src/Exceptionless.Web/Utility/Delta/Delta.cs @@ -2,10 +2,9 @@ using System.Collections.Concurrent; using System.Dynamic; +using System.Text.Json; using Exceptionless.Core.Extensions; using Exceptionless.Core.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Exceptionless.Web.Utility; @@ -79,11 +78,11 @@ public bool TrySetPropertyValue(string name, object? value, TEntityType? target if (value is not null) { - if (value is JToken jToken) + if (value is JsonElement jsonElement) { try { - value = JsonConvert.DeserializeObject(jToken.ToString(), cacheHit.MemberType); + value = JsonSerializer.Deserialize(jsonElement.GetRawText(), cacheHit.MemberType); } catch (Exception) { diff --git a/src/Exceptionless.Web/Utility/Delta/DeltaJsonConverter.cs b/src/Exceptionless.Web/Utility/Delta/DeltaJsonConverter.cs new file mode 100644 index 0000000000..66321c608e --- /dev/null +++ b/src/Exceptionless.Web/Utility/Delta/DeltaJsonConverter.cs @@ -0,0 +1,133 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Exceptionless.Web.Utility; + +/// +/// JsonConverterFactory for Delta<T> types to support System.Text.Json deserialization. +/// +public class DeltaJsonConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + if (!typeToConvert.IsGenericType) + { + return false; + } + + return typeToConvert.GetGenericTypeDefinition() == typeof(Delta<>); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var entityType = typeToConvert.GetGenericArguments()[0]; + var converterType = typeof(DeltaJsonConverter<>).MakeGenericType(entityType); + + return (JsonConverter?)Activator.CreateInstance(converterType, options); + } +} + +/// +/// JsonConverter for Delta<T> that reads JSON properties and sets them on the Delta instance. +/// +public class DeltaJsonConverter : JsonConverter> where TEntityType : class +{ + private readonly JsonSerializerOptions _options; + private readonly Dictionary _jsonNameToPropertyName; + + public DeltaJsonConverter(JsonSerializerOptions options) + { + // Create a copy without the converter to avoid infinite recursion + _options = new JsonSerializerOptions(options); + + // Build a mapping from JSON property names (snake_case) to C# property names (PascalCase) + _jsonNameToPropertyName = new Dictionary(StringComparer.OrdinalIgnoreCase); + var entityType = typeof(TEntityType); + foreach (var prop in entityType.GetProperties()) + { + var jsonName = options.PropertyNamingPolicy?.ConvertName(prop.Name) ?? prop.Name; + _jsonNameToPropertyName[jsonName] = prop.Name; + } + } + + public override Delta? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Expected StartObject token"); + } + + var delta = new Delta(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException("Expected PropertyName token"); + } + + var jsonPropertyName = reader.GetString(); + if (jsonPropertyName is null) + { + throw new JsonException("Property name is null"); + } + + reader.Read(); + + // Convert JSON property name (snake_case) to C# property name (PascalCase) + var propertyName = _jsonNameToPropertyName.TryGetValue(jsonPropertyName, out var mapped) + ? mapped + : jsonPropertyName; + + // Try to get the property type from Delta + if (delta.TryGetPropertyType(propertyName, out var propertyType) && propertyType is not null) + { + var value = JsonSerializer.Deserialize(ref reader, propertyType, _options); + delta.TrySetPropertyValue(propertyName, value); + } + else + { + // Unknown property - read and store as JsonElement + var element = JsonSerializer.Deserialize(ref reader, _options); + delta.UnknownProperties[jsonPropertyName] = element; + } + } + + return delta; + } + + public override void Write(Utf8JsonWriter writer, Delta value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var propertyName in value.GetChangedPropertyNames()) + { + if (value.TryGetPropertyValue(propertyName, out var propertyValue)) + { + // Convert property name to snake_case if needed + var jsonPropertyName = options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName; + writer.WritePropertyName(jsonPropertyName); + JsonSerializer.Serialize(writer, propertyValue, _options); + } + } + + foreach (var kvp in value.UnknownProperties) + { + var jsonPropertyName = options.PropertyNamingPolicy?.ConvertName(kvp.Key) ?? kvp.Key; + writer.WritePropertyName(jsonPropertyName); + JsonSerializer.Serialize(writer, kvp.Value, _options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Exceptionless.Web/Utility/Handlers/AllowSynchronousIOMiddleware.cs b/src/Exceptionless.Web/Utility/Handlers/AllowSynchronousIOMiddleware.cs deleted file mode 100644 index a32e12eac5..0000000000 --- a/src/Exceptionless.Web/Utility/Handlers/AllowSynchronousIOMiddleware.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Http.Features; - -namespace Exceptionless.Web.Utility.Handlers; - -public class AllowSynchronousIOMiddleware -{ - private readonly RequestDelegate _next; - - public AllowSynchronousIOMiddleware(RequestDelegate next) - { - _next = next; - } - - public Task Invoke(HttpContext context) - { - var syncIOFeature = context.Features.Get(); - if (syncIOFeature is not null) - syncIOFeature.AllowSynchronousIO = true; - - return _next(context); - } -} diff --git a/src/Exceptionless.Web/Utility/LowerCaseUnderscoreNamingPolicy.cs b/src/Exceptionless.Web/Utility/LowerCaseUnderscoreNamingPolicy.cs new file mode 100644 index 0000000000..1f4d0d53fe --- /dev/null +++ b/src/Exceptionless.Web/Utility/LowerCaseUnderscoreNamingPolicy.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using Exceptionless.Core.Extensions; + +namespace Exceptionless.Web.Utility; + +/// +/// A JSON naming policy that converts PascalCase to lower_case_underscore format. +/// This uses the existing ToLowerUnderscoredWords extension method to maintain +/// API compatibility with legacy Newtonsoft.Json serialization. +/// +/// Note: This implementation treats each uppercase letter individually, so: +/// - "OSName" becomes "o_s_name" (not "os_name") +/// - "EnableSSL" becomes "enable_s_s_l" (not "enable_ssl") +/// - "BaseURL" becomes "base_u_r_l" (not "base_url") +/// - "PropertyName" becomes "property_name" +/// +/// This matches the legacy behavior. See https://github.com/exceptionless/Exceptionless.Net/issues/2 +/// for discussion on future improvements. +/// +public sealed class LowerCaseUnderscoreNamingPolicy : JsonNamingPolicy +{ + public static LowerCaseUnderscoreNamingPolicy Instance { get; } = new(); + + public override string ConvertName(string name) + { + return name.ToLowerUnderscoredWords(); + } +} diff --git a/src/Exceptionless.Web/Utility/OpenApi/DeltaSchemaTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/DeltaSchemaTransformer.cs new file mode 100644 index 0000000000..b85f61c014 --- /dev/null +++ b/src/Exceptionless.Web/Utility/OpenApi/DeltaSchemaTransformer.cs @@ -0,0 +1,116 @@ +using System.Reflection; +using Exceptionless.Core.Extensions; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace Exceptionless.Web.Utility.OpenApi; + +/// +/// Schema transformer that populates Delta<T> schemas with the properties from T. +/// All properties are optional to represent PATCH semantics (partial updates). +/// +public class DeltaSchemaTransformer : IOpenApiSchemaTransformer +{ + public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) + { + var type = context.JsonTypeInfo.Type; + + // Check if this is a Delta type + if (!IsDeltaType(type)) + { + return Task.CompletedTask; + } + + // Get the inner type T from Delta + var innerType = type.GetGenericArguments().FirstOrDefault(); + if (innerType is null) + { + return Task.CompletedTask; + } + + // Set the type to object + schema.Type = JsonSchemaType.Object; + + // Add properties from the inner type + schema.Properties ??= new Dictionary(); + + foreach (var property in innerType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (!property.CanRead || !property.CanWrite) + { + continue; + } + + var propertySchema = CreateSchemaForType(property.PropertyType); + var propertyName = property.Name.ToLowerUnderscoredWords(); + + schema.Properties[propertyName] = propertySchema; + } + + // Ensure no required array - all properties are optional for PATCH + schema.Required = null; + + return Task.CompletedTask; + } + + private static bool IsDeltaType(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Delta<>); + } + + private static OpenApiSchema CreateSchemaForType(Type type) + { + var schema = new OpenApiSchema(); + JsonSchemaType schemaType = default; + + // Handle nullable types + var underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType is not null) + { + type = underlyingType; + schemaType |= JsonSchemaType.Null; + } + + if (type == typeof(string)) + { + schemaType |= JsonSchemaType.String; + } + else if (type == typeof(bool)) + { + schemaType |= JsonSchemaType.Boolean; + } + else if (type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte)) + { + schemaType |= JsonSchemaType.Integer; + } + else if (type == typeof(double) || type == typeof(float) || type == typeof(decimal)) + { + schemaType |= JsonSchemaType.Number; + } + else if (type == typeof(DateTime) || type == typeof(DateTimeOffset)) + { + schemaType |= JsonSchemaType.String; + schema.Format = "date-time"; + } + else if (type == typeof(Guid)) + { + schemaType |= JsonSchemaType.String; + schema.Format = "uuid"; + } + else if (type.IsEnum) + { + schemaType |= JsonSchemaType.String; + } + else if (type.IsArray || (type.IsGenericType && typeof(System.Collections.IEnumerable).IsAssignableFrom(type))) + { + schemaType = JsonSchemaType.Array; + } + else + { + schemaType = JsonSchemaType.Object; + } + + schema.Type = schemaType; + return schema; + } +} diff --git a/src/Exceptionless.Web/Utility/OpenApi/DocumentInfoTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/DocumentInfoTransformer.cs new file mode 100644 index 0000000000..d12130bd67 --- /dev/null +++ b/src/Exceptionless.Web/Utility/OpenApi/DocumentInfoTransformer.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace Exceptionless.Web.Utility.OpenApi; + +/// +/// Document transformer that adds API information and security schemes to the OpenAPI document. +/// +public class DocumentInfoTransformer : IOpenApiDocumentTransformer +{ + public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + document.Info = new OpenApiInfo + { + Title = "Exceptionless API", + Version = "v2", + TermsOfService = new Uri("https://exceptionless.com/terms/"), + Contact = new OpenApiContact + { + Name = "Exceptionless", + Email = String.Empty, + Url = new Uri("https://github.com/exceptionless/Exceptionless") + }, + License = new OpenApiLicense + { + Name = "Apache License 2.0", + Url = new Uri("https://github.com/exceptionless/Exceptionless/blob/main/LICENSE.txt") + } + }; + + document.Components ??= new OpenApiComponents(); + document.Components.SecuritySchemes = new Dictionary + { + ["Basic"] = new OpenApiSecurityScheme + { + Description = "Basic HTTP Authentication", + Scheme = "basic", + Type = SecuritySchemeType.Http + }, + ["Bearer"] = new OpenApiSecurityScheme + { + Description = "Authorization token. Example: \"Bearer {apikey}\"", + Scheme = "bearer", + Type = SecuritySchemeType.Http + }, + ["Token"] = new OpenApiSecurityScheme + { + Description = "Authorization token. Example: \"Bearer {apikey}\"", + Name = "access_token", + In = ParameterLocation.Query, + Type = SecuritySchemeType.ApiKey + } + }; + + // Add top-level security requirement (applies to all operations) + document.Security ??= []; + document.Security.Add(new OpenApiSecurityRequirement + { + [new OpenApiSecuritySchemeReference("Basic", document)] = [], + [new OpenApiSecuritySchemeReference("Bearer", document)] = [], + [new OpenApiSecuritySchemeReference("Token", document)] = [] + }); + + return Task.CompletedTask; + } +} diff --git a/src/Exceptionless.Web/Utility/OpenApi/ReadOnlyPropertySchemaTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/ReadOnlyPropertySchemaTransformer.cs new file mode 100644 index 0000000000..447f955b78 --- /dev/null +++ b/src/Exceptionless.Web/Utility/OpenApi/ReadOnlyPropertySchemaTransformer.cs @@ -0,0 +1,48 @@ +using Exceptionless.Core.Extensions; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace Exceptionless.Web.Utility.OpenApi; + +/// +/// Schema transformer that adds readOnly: true to properties that have only getters (no setters). +/// This helps API consumers understand which properties are computed and cannot be set. +/// +public class ReadOnlyPropertySchemaTransformer : IOpenApiSchemaTransformer +{ + public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) + { + if (schema.Properties is null || schema.Properties.Count == 0) + { + return Task.CompletedTask; + } + + var type = context.JsonTypeInfo.Type; + if (type is null || !type.IsClass) + { + return Task.CompletedTask; + } + + foreach (var property in type.GetProperties()) + { + if (!property.CanRead || property.CanWrite) + { + continue; + } + + // Find the matching schema property (property names are in snake_case in the schema) + var schemaPropertyName = property.Name.ToLowerUnderscoredWords(); + + if (schema.Properties.TryGetValue(schemaPropertyName, out var propertySchema)) + { + // Cast to OpenApiSchema to access mutable properties + if (propertySchema is OpenApiSchema mutableSchema) + { + mutableSchema.ReadOnly = true; + } + } + } + + return Task.CompletedTask; + } +} diff --git a/src/Exceptionless.Web/Utility/RemoveProblemJsonFromSuccessResponsesFilter.cs b/src/Exceptionless.Web/Utility/OpenApi/RemoveProblemJsonFromSuccessResponsesTransformer.cs similarity index 57% rename from src/Exceptionless.Web/Utility/RemoveProblemJsonFromSuccessResponsesFilter.cs rename to src/Exceptionless.Web/Utility/OpenApi/RemoveProblemJsonFromSuccessResponsesTransformer.cs index d3b0983f0a..9e3dfcf0c1 100644 --- a/src/Exceptionless.Web/Utility/RemoveProblemJsonFromSuccessResponsesFilter.cs +++ b/src/Exceptionless.Web/Utility/OpenApi/RemoveProblemJsonFromSuccessResponsesTransformer.cs @@ -1,30 +1,36 @@ +using Microsoft.AspNetCore.OpenApi; using Microsoft.OpenApi; -using Swashbuckle.AspNetCore.SwaggerGen; -namespace Exceptionless.Web.Utility; +namespace Exceptionless.Web.Utility.OpenApi; /// -/// Removes application/problem+json content type from successful (2xx) responses. +/// Document transformer that removes application/problem+json content type from successful (2xx) responses. /// The problem+json media type (RFC 7807) should only be used for error responses. /// -public class RemoveProblemJsonFromSuccessResponsesFilter : IDocumentFilter +public class RemoveProblemJsonFromSuccessResponsesTransformer : IOpenApiDocumentTransformer { private const string ProblemJsonContentType = "application/problem+json"; - public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) { - if (swaggerDoc.Paths is null) - return; + if (document.Paths is null) + { + return Task.CompletedTask; + } - foreach (var path in swaggerDoc.Paths) + foreach (var path in document.Paths) { if (path.Value?.Operations is null) + { continue; + } foreach (var operation in path.Value.Operations.Values) { if (operation?.Responses is null) + { continue; + } foreach (var response in operation.Responses) { @@ -36,5 +42,7 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) } } } + + return Task.CompletedTask; } } diff --git a/src/Exceptionless.Web/Utility/OpenApi/RequestBodyContentOperationTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/RequestBodyContentOperationTransformer.cs new file mode 100644 index 0000000000..cecb0a9eec --- /dev/null +++ b/src/Exceptionless.Web/Utility/OpenApi/RequestBodyContentOperationTransformer.cs @@ -0,0 +1,71 @@ +using System.Reflection; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace Exceptionless.Web.Utility.OpenApi; + +/// +/// Operation transformer that handles endpoints with [RequestBodyContent] attribute +/// to properly set the request body schema for raw content types. +/// +public class RequestBodyContentOperationTransformer : IOpenApiOperationTransformer +{ + public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken) + { + var methodInfo = context.Description.ActionDescriptor.EndpointMetadata + .OfType() + .FirstOrDefault(); + + if (methodInfo is null) + { + // For controller actions, try to get from ControllerActionDescriptor + if (context.Description.ActionDescriptor is Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor controllerDescriptor) + { + methodInfo = controllerDescriptor.MethodInfo; + } + } + + if (methodInfo is null) + { + return Task.CompletedTask; + } + + var hasRequestBodyContent = methodInfo.GetCustomAttributes(typeof(RequestBodyContentAttribute), true).Any(); + if (!hasRequestBodyContent) + { + return Task.CompletedTask; + } + + var consumesAttribute = methodInfo.GetCustomAttributes(typeof(ConsumesAttribute), true).FirstOrDefault() as ConsumesAttribute; + if (consumesAttribute is null) + { + return Task.CompletedTask; + } + + operation.RequestBody = new OpenApiRequestBody + { + Required = true, + Content = new Dictionary() + }; + + foreach (string contentType in consumesAttribute.ContentTypes) + { + operation.RequestBody.Content!.Add(contentType, new OpenApiMediaType + { + Schema = new OpenApiSchema { Type = JsonSchemaType.String, Example = JsonValue.Create(String.Empty) } + }); + } + + return Task.CompletedTask; + } +} + +/// +/// Attribute to mark endpoints that accept raw request body content. +/// +[AttributeUsage(AttributeTargets.Method)] +public class RequestBodyContentAttribute : Attribute +{ +} diff --git a/src/Exceptionless.Web/Utility/OpenApi/UniqueItemsSchemaTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/UniqueItemsSchemaTransformer.cs new file mode 100644 index 0000000000..a04d98c8b8 --- /dev/null +++ b/src/Exceptionless.Web/Utility/OpenApi/UniqueItemsSchemaTransformer.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace Exceptionless.Web.Utility.OpenApi; + +/// +/// Schema transformer that adds uniqueItems: true to HashSet and ISet properties. +/// This maintains compatibility with the previous Swashbuckle-generated schema. +/// +public class UniqueItemsSchemaTransformer : IOpenApiSchemaTransformer +{ + public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) + { + var type = context.JsonTypeInfo.Type; + + // Check if this is a Set type (HashSet, ISet, etc.) + if (IsSetType(type)) + { + schema.UniqueItems = true; + } + + return Task.CompletedTask; + } + + private static bool IsSetType(Type type) + { + if (type.IsGenericType) + { + var genericTypeDef = type.GetGenericTypeDefinition(); + if (genericTypeDef == typeof(HashSet<>) || + genericTypeDef == typeof(ISet<>) || + genericTypeDef == typeof(SortedSet<>)) + { + return true; + } + } + + // Check if it implements ISet + return type.GetInterfaces().Any(i => + i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)); + } +} diff --git a/src/Exceptionless.Web/Utility/OpenApi/XEnumNamesSchemaTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/XEnumNamesSchemaTransformer.cs new file mode 100644 index 0000000000..857100da04 --- /dev/null +++ b/src/Exceptionless.Web/Utility/OpenApi/XEnumNamesSchemaTransformer.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace Exceptionless.Web.Utility.OpenApi; + +/// +/// Schema transformer that adds x-enumNames extension to enum schemas. +/// This enables swagger-typescript-api and similar generators to create +/// meaningful enum member names instead of Value0, Value1, etc. +/// +public class XEnumNamesSchemaTransformer : IOpenApiSchemaTransformer +{ + public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) + { + var type = context.JsonTypeInfo.Type; + if (!type.IsEnum) + { + return Task.CompletedTask; + } + + if (schema.Enum is null || schema.Enum.Count == 0) + { + return Task.CompletedTask; + } + + var names = Enum.GetNames(type); + var enumNamesArray = new JsonArray(); + + foreach (var name in names) + { + enumNamesArray.Add(name); + } + + schema.Extensions ??= new Dictionary(); + schema.Extensions["x-enumNames"] = new JsonNodeExtension(enumNamesArray); + + return Task.CompletedTask; + } +} diff --git a/src/Exceptionless.Web/Utility/OpenApi/XmlDocumentationOperationTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/XmlDocumentationOperationTransformer.cs new file mode 100644 index 0000000000..645e8f8650 --- /dev/null +++ b/src/Exceptionless.Web/Utility/OpenApi/XmlDocumentationOperationTransformer.cs @@ -0,0 +1,165 @@ +using System.Reflection; +using System.Xml.Linq; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace Exceptionless.Web.Utility.OpenApi; + +/// +/// Operation transformer that reads XML documentation <response> tags +/// and adds them to OpenAPI operation responses. +/// +public class XmlDocumentationOperationTransformer : IOpenApiOperationTransformer +{ + private static readonly Dictionary _xmlDocCache = new(); + private static readonly object _lock = new(); + + public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken) + { + var methodInfo = context.Description.ActionDescriptor.EndpointMetadata + .OfType() + .FirstOrDefault(); + + if (methodInfo is null) + { + // For controller actions, try to get from ControllerActionDescriptor + if (context.Description.ActionDescriptor is Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor controllerDescriptor) + { + methodInfo = controllerDescriptor.MethodInfo; + } + } + + if (methodInfo is null) + { + return Task.CompletedTask; + } + + var xmlDoc = GetXmlDocumentation(methodInfo.DeclaringType?.Assembly); + if (xmlDoc is null) + { + return Task.CompletedTask; + } + + var methodMemberName = GetMemberName(methodInfo); + var memberElement = xmlDoc.Descendants("member") + .FirstOrDefault(m => m.Attribute("name")?.Value == methodMemberName); + + if (memberElement is null) + { + return Task.CompletedTask; + } + + var responseElements = memberElement.Elements("response"); + foreach (var responseElement in responseElements) + { + var codeAttribute = responseElement.Attribute("code"); + if (codeAttribute is null) + { + continue; + } + + var statusCode = codeAttribute.Value; + var description = responseElement.Value.Trim(); + + // Skip if Responses is null or this response already exists + if (operation.Responses is null || operation.Responses.ContainsKey(statusCode)) + { + continue; + } + + operation.Responses[statusCode] = new OpenApiResponse + { + Description = description + }; + } + + return Task.CompletedTask; + } + + private static XDocument? GetXmlDocumentation(Assembly? assembly) + { + if (assembly is null) + { + return null; + } + + var assemblyName = assembly.GetName().Name; + if (assemblyName is null) + { + return null; + } + + lock (_lock) + { + if (_xmlDocCache.TryGetValue(assemblyName, out var cachedDoc)) + { + return cachedDoc; + } + + var xmlPath = Path.Combine(AppContext.BaseDirectory, $"{assemblyName}.xml"); + if (!File.Exists(xmlPath)) + { + return null; + } + + try + { + var doc = XDocument.Load(xmlPath); + _xmlDocCache[assemblyName] = doc; + return doc; + } + catch + { + return null; + } + } + } + + private static string GetMemberName(MethodInfo methodInfo) + { + var declaringType = methodInfo.DeclaringType; + if (declaringType is null) + { + return string.Empty; + } + + var typeName = declaringType.FullName?.Replace('+', '.'); + var parameters = methodInfo.GetParameters(); + + if (parameters.Length == 0) + { + return $"M:{typeName}.{methodInfo.Name}"; + } + + var parameterTypes = string.Join(",", parameters.Select(p => GetParameterTypeName(p.ParameterType))); + return $"M:{typeName}.{methodInfo.Name}({parameterTypes})"; + } + + private static string GetParameterTypeName(Type type) + { + if (type.IsGenericType) + { + var genericTypeName = type.GetGenericTypeDefinition().FullName; + if (genericTypeName is null) + { + return type.Name; + } + + var backtickIndex = genericTypeName.IndexOf('`'); + if (backtickIndex > 0) + { + genericTypeName = genericTypeName[..backtickIndex]; + } + + var genericArgs = string.Join(",", type.GetGenericArguments().Select(GetParameterTypeName)); + return $"{genericTypeName}{{{genericArgs}}}"; + } + + if (type.IsArray) + { + return $"{GetParameterTypeName(type.GetElementType()!)}[]"; + } + + return type.FullName ?? type.Name; + } +} diff --git a/src/Exceptionless.Web/Utility/RequestBodyOperationFilter.cs b/src/Exceptionless.Web/Utility/RequestBodyOperationFilter.cs deleted file mode 100644 index 3298480620..0000000000 --- a/src/Exceptionless.Web/Utility/RequestBodyOperationFilter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text.Json.Nodes; -using Microsoft.AspNetCore.Mvc; -using Microsoft.OpenApi; -using Swashbuckle.AspNetCore.SwaggerGen; - -public class RequestBodyContentAttribute : Attribute -{ -} - -public class RequestBodyOperationFilter : IOperationFilter -{ - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - object? attributes = context.MethodInfo.GetCustomAttributes(typeof(RequestBodyContentAttribute), true).FirstOrDefault(); - if (attributes is null) - return; - - var consumesAttribute = context.MethodInfo.GetCustomAttributes(typeof(ConsumesAttribute), true).FirstOrDefault() as ConsumesAttribute; - if (consumesAttribute is null) - return; - - operation.RequestBody = new OpenApiRequestBody - { - Required = true, - Content = new Dictionary() - }; - - foreach (string contentType in consumesAttribute.ContentTypes) - { - operation.RequestBody.Content!.Add(contentType, new OpenApiMediaType - { - Schema = new OpenApiSchema { Type = JsonSchemaType.String, Example = JsonValue.Create(String.Empty) } - }); - } - } -} diff --git a/src/Exceptionless.Web/Utility/XEnumNamesSchemaFilter.cs b/src/Exceptionless.Web/Utility/XEnumNamesSchemaFilter.cs deleted file mode 100644 index b1594a49e6..0000000000 --- a/src/Exceptionless.Web/Utility/XEnumNamesSchemaFilter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text.Json.Nodes; -using Microsoft.OpenApi; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Exceptionless.Web.Utility; - -/// -/// Schema filter that adds x-enumNames extension to numeric enum schemas. -/// This enables swagger-typescript-api and similar generators to create -/// meaningful enum member names instead of Value0, Value1, etc. -/// -public class XEnumNamesSchemaFilter : ISchemaFilter -{ - public void Apply(IOpenApiSchema schema, SchemaFilterContext context) - { - if (schema is not OpenApiSchema concrete) - return; - - var type = context.Type; - if (type is null || !type.IsEnum) - return; - - if (concrete.Enum is null || concrete.Enum.Count == 0) - return; - - var names = Enum.GetNames(type); - var enumNamesArray = new JsonArray(); - - foreach (var name in names) - { - enumNamesArray.Add(name); - } - - concrete.Extensions ??= new Dictionary(); - concrete.Extensions["x-enumNames"] = new JsonNodeExtension(enumNamesArray); - } -} diff --git a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs index 158c804757..b4e0c51f9f 100644 --- a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs @@ -48,6 +48,8 @@ protected override async Task ResetDataAsync() [Fact] public async Task CannotSignupWithoutPassword() { + // With System.Text.Json's RespectNullableAnnotations, missing required properties + // fail at deserialization (400 BadRequest) rather than validation (422 UnprocessableEntity) var problemDetails = await SendRequestAsAsync(r => r .Post() .AppendPath("auth/signup") @@ -57,12 +59,13 @@ public async Task CannotSignupWithoutPassword() Email = "test@domain.com", Password = null! }) - .StatusCodeShouldBeUnprocessableEntity() + .StatusCodeShouldBeBadRequest() ); Assert.NotNull(problemDetails); - Assert.Single(problemDetails.Errors); - Assert.Contains(problemDetails.Errors, error => String.Equals(error.Key, "password")); + Assert.NotEmpty(problemDetails.Errors); + // System.Text.Json deserialization errors use the property name as the key + Assert.Contains(problemDetails.Errors, error => error.Key.Contains("password", StringComparison.OrdinalIgnoreCase)); } [Theory] @@ -450,6 +453,8 @@ public async Task SignupShouldFailWhenUsingExistingAccountWithNoPasswordOrInvali user.MarkEmailAddressVerified(); await _userRepository.AddAsync(user); + // With System.Text.Json's RespectNullableAnnotations, missing required properties + // fail at deserialization (400 BadRequest) rather than validation (422 UnprocessableEntity) var problemDetails = await SendRequestAsAsync(r => r .Post() .AppendPath("auth/signup") @@ -459,12 +464,13 @@ public async Task SignupShouldFailWhenUsingExistingAccountWithNoPasswordOrInvali Email = email, Password = null! }) - .StatusCodeShouldBeUnprocessableEntity() + .StatusCodeShouldBeBadRequest() ); Assert.NotNull(problemDetails); - Assert.Single(problemDetails.Errors); - Assert.Contains(problemDetails.Errors, error => String.Equals(error.Key, "password")); + Assert.NotEmpty(problemDetails.Errors); + // System.Text.Json deserialization errors use the property name as the key + Assert.Contains(problemDetails.Errors, error => error.Key.Contains("password", StringComparison.OrdinalIgnoreCase)); await SendRequestAsync(r => r .Post() diff --git a/tests/Exceptionless.Tests/Controllers/Data/swagger.json b/tests/Exceptionless.Tests/Controllers/Data/openapi.json similarity index 80% rename from tests/Exceptionless.Tests/Controllers/Data/swagger.json rename to tests/Exceptionless.Tests/Controllers/Data/openapi.json index a27574c17b..a20be57cd1 100644 --- a/tests/Exceptionless.Tests/Controllers/Data/swagger.json +++ b/tests/Exceptionless.Tests/Controllers/Data/openapi.json @@ -1,5 +1,5 @@ { - "openapi": "3.0.4", + "openapi": "3.1.1", "info": { "title": "Exceptionless API", "termsOfService": "https://exceptionless.com/terms/", @@ -14,443 +14,581 @@ }, "version": "v2" }, + "servers": [ + { + "url": "http://localhost/" + } + ], "paths": { - "/api/v2/auth/login": { - "post": { + "/api/v2/organizations/{organizationId}/tokens": { + "get": { "tags": [ - "Auth" + "Token" ], - "summary": "Login", - "description": "Log in with your email address and password to generate a token scoped with your users roles.\n \n```{ \"email\": \"noreply@exceptionless.io\", \"password\": \"exceptionless\" }```\n \nThis token can then be used to access the api. You can use this token in the header (bearer authentication)\nor append it onto the query string: ?access_token=MY_TOKEN\n \nPlease note that you can also use this token on the documentation site by placing it in the\nheaders api_key input box.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Login" - } + "summary": "Get by organization", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "description": "The identifier of the organization.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "The page parameter is used for pagination. This value must be greater than 0.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 10 } } - }, + ], "responses": { "200": { - "description": "User Authentication Token", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokenResult" + "type": "array", + "items": { + "$ref": "#/components/schemas/ViewToken" + } } } } }, - "401": { - "description": "Login failed" - }, - "422": { - "description": "Validation error" + "404": { + "description": "The organization could not be found." } } - } - }, - "/api/v2/auth/logout": { - "get": { + }, + "post": { "tags": [ - "Auth" + "Token" ], - "summary": "Logout the current user and remove the current access token", - "responses": { - "200": { - "description": "User successfully logged-out" - }, - "401": { - "description": "User not logged in" - }, - "403": { - "description": "Current action is not supported with user access token" + "summary": "Create for organization", + "description": "This is a helper action that makes it easier to create a token for a specific organization.\nYou may also specify a scope when creating a token. There are three valid scopes: client, user and admin.", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "description": "The identifier of the organization.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } } - } - } - }, - "/api/v2/auth/signup": { - "post": { - "tags": [ - "Auth" ], - "summary": "Sign up", "requestBody": { + "description": "The token.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Signup" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NewToken" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NewToken" + } + ] } } } }, "responses": { - "200": { - "description": "User Authentication Token", + "201": { + "description": "Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokenResult" + "$ref": "#/components/schemas/ViewToken" } } } }, - "401": { - "description": "Sign-up failed" - }, - "403": { - "description": "Account Creation is currently disabled" + "400": { + "description": "An error occurred while creating the token." }, - "422": { - "description": "Validation error" + "409": { + "description": "The token already exists." } } } }, - "/api/v2/auth/github": { - "post": { + "/api/v2/projects/{projectId}/tokens": { + "get": { "tags": [ - "Auth" + "Token" ], - "summary": "Sign in with GitHub", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExternalAuthInfo" - } + "summary": "Get by project", + "parameters": [ + { + "name": "projectId", + "in": "path", + "description": "The identifier of the project.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "The page parameter is used for pagination. This value must be greater than 0.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 10 } } - }, + ], "responses": { "200": { - "description": "User Authentication Token", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokenResult" + "type": "array", + "items": { + "$ref": "#/components/schemas/ViewToken" + } } } } }, - "403": { - "description": "Account Creation is currently disabled" - }, - "422": { - "description": "Validation error" + "404": { + "description": "The project could not be found." } } - } - }, - "/api/v2/auth/google": { + }, "post": { "tags": [ - "Auth" + "Token" + ], + "summary": "Create for project", + "description": "This is a helper action that makes it easier to create a token for a specific project.\nYou may also specify a scope when creating a token. There are three valid scopes: client, user and admin.", + "parameters": [ + { + "name": "projectId", + "in": "path", + "description": "The identifier of the project.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + } ], - "summary": "Sign in with Google", "requestBody": { + "description": "The token.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExternalAuthInfo" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NewToken" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NewToken" + } + ] } } } }, "responses": { - "200": { - "description": "User Authentication Token", + "201": { + "description": "Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokenResult" + "$ref": "#/components/schemas/ViewToken" } } } }, - "403": { - "description": "Account Creation is currently disabled" + "400": { + "description": "An error occurred while creating the token." }, - "422": { - "description": "Validation error" + "404": { + "description": "The project could not be found." + }, + "409": { + "description": "The token already exists." } } } }, - "/api/v2/auth/facebook": { - "post": { + "/api/v2/projects/{projectId}/tokens/default": { + "get": { "tags": [ - "Auth" + "Token" ], - "summary": "Sign in with Facebook", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExternalAuthInfo" - } + "summary": "Get a projects default token", + "parameters": [ + { + "name": "projectId", + "in": "path", + "description": "The identifier of the project.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" } } - }, + ], "responses": { "200": { - "description": "User Authentication Token", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokenResult" + "$ref": "#/components/schemas/ViewToken" } } } }, - "403": { - "description": "Account Creation is currently disabled" - }, - "422": { - "description": "Validation error" + "404": { + "description": "The project could not be found." } } } }, - "/api/v2/auth/live": { - "post": { + "/api/v2/tokens/{id}": { + "get": { "tags": [ - "Auth" + "Token" ], - "summary": "Sign in with Microsoft", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExternalAuthInfo" - } + "summary": "Get by id", + "operationId": "GetTokenById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The identifier of the token.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d-]{24,40}$", + "type": "string" } } - }, + ], "responses": { "200": { - "description": "User Authentication Token", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokenResult" + "$ref": "#/components/schemas/ViewToken" } } } }, - "403": { - "description": "Account Creation is currently disabled" - }, - "422": { - "description": "Validation error" + "404": { + "description": "The token could not be found." } } - } - }, - "/api/v2/auth/unlink/{providerName}": { - "post": { + }, + "patch": { "tags": [ - "Auth" + "Token" ], - "summary": "Removes an external login provider from the account", + "summary": "Update", "parameters": [ { - "name": "providerName", + "name": "id", "in": "path", - "description": "The provider name.", + "description": "The identifier of the token.", "required": true, "schema": { - "minLength": 1, + "pattern": "^[a-zA-Z\\d-]{24,40}(,[a-zA-Z\\d-]{24,40})*$", "type": "string" } } ], "requestBody": { - "description": "The provider user id.", + "description": "The changes", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StringValueFromBody" + "$ref": "#/components/schemas/UpdateToken" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateToken" } } - } + }, + "required": true }, "responses": { "200": { - "description": "User Authentication Token", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokenResult" + "$ref": "#/components/schemas/ViewToken" } } } }, "400": { - "description": "Invalid provider name." + "description": "An error occurred while updating the token." + }, + "404": { + "description": "The token could not be found." } } - } - }, - "/api/v2/auth/change-password": { - "post": { + }, + "put": { "tags": [ - "Auth" + "Token" + ], + "summary": "Update", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The identifier of the token.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d-]{24,40}(,[a-zA-Z\\d-]{24,40})*$", + "type": "string" + } + } ], - "summary": "Change password", "requestBody": { + "description": "The changes", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChangePasswordModel" + "$ref": "#/components/schemas/UpdateToken" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateToken" } } - } + }, + "required": true }, "responses": { "200": { - "description": "User Authentication Token", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokenResult" + "$ref": "#/components/schemas/ViewToken" } } } }, - "422": { - "description": "Validation error" - } - } - } - }, - "/api/v2/auth/forgot-password/{email}": { - "get": { - "tags": [ - "Auth" - ], - "summary": "Forgot password", - "parameters": [ - { - "name": "email", - "in": "path", - "description": "The email address.", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Forgot password email was sent." - }, "400": { - "description": "Invalid email address." + "description": "An error occurred while updating the token." + }, + "404": { + "description": "The token could not be found." } } } }, - "/api/v2/auth/reset-password": { + "/api/v2/tokens": { "post": { "tags": [ - "Auth" + "Token" ], - "summary": "Reset password", + "summary": "Create", + "description": "To create a new token, you must specify an organization_id. There are three valid scopes: client, user and admin.", "requestBody": { + "description": "The token.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResetPasswordModel" + "$ref": "#/components/schemas/NewToken" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NewToken" } } - } + }, + "required": true }, "responses": { - "200": { - "description": "Password reset email was sent." + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewToken" + } + } + } }, - "422": { - "description": "Invalid reset password model." + "400": { + "description": "An error occurred while creating the token." + }, + "409": { + "description": "The token already exists." } } } }, - "/api/v2/auth/cancel-reset-password/{token}": { - "post": { + "/api/v2/tokens/{ids}": { + "delete": { "tags": [ - "Auth" + "Token" ], - "summary": "Cancel reset password", + "summary": "Remove", "parameters": [ { - "name": "token", + "name": "ids", "in": "path", - "description": "The password reset token.", + "description": "A comma-delimited list of token identifiers.", "required": true, "schema": { - "minLength": 1, + "pattern": "^[a-zA-Z\\d-]{24,40}(,[a-zA-Z\\d-]{24,40})*$", "type": "string" } } ], "responses": { - "200": { - "description": "Password reset email was cancelled." + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkInProgressResult" + } + } + } }, "400": { - "description": "Invalid password reset token." + "description": "One or more validation errors occurred." + }, + "404": { + "description": "One or more tokens were not found." + }, + "500": { + "description": "An error occurred while deleting one or more tokens." } } } }, - "/api/v2/events/count": { + "/api/v2/projects/{projectId}/webhooks": { "get": { "tags": [ - "Event" + "WebHook" ], - "summary": "Count", + "summary": "Get by project", "parameters": [ { - "name": "filter", - "in": "query", - "description": "A filter that controls what data is returned from the server.", - "schema": { - "type": "string" - } - }, - { - "name": "aggregations", - "in": "query", - "description": "A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value", - "schema": { - "type": "string" - } - }, - { - "name": "time", - "in": "query", - "description": "The time filter that limits the data being returned to a specific date range.", + "name": "projectId", + "in": "path", + "description": "The identifier of the project.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } }, { - "name": "offset", + "name": "page", "in": "query", - "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "string" + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 1 } }, { - "name": "mode", + "name": "limit", "in": "query", - "description": "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "string" + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 10 } } ], @@ -460,73 +598,37 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CountResult" + "type": "array", + "items": { + "$ref": "#/components/schemas/WebHook" + } } } } }, - "400": { - "description": "Invalid filter." + "404": { + "description": "The project could not be found." } } } }, - "/api/v2/organizations/{organizationId}/events/count": { + "/api/v2/webhooks/{id}": { "get": { "tags": [ - "Event" + "WebHook" ], - "summary": "Count by organization", + "summary": "Get by id", + "operationId": "GetWebHookById", "parameters": [ { - "name": "organizationId", + "name": "id", "in": "path", - "description": "The identifier of the organization.", + "description": "The identifier of the web hook.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - }, - { - "name": "filter", - "in": "query", - "description": "A filter that controls what data is returned from the server.", - "schema": { - "type": "string" - } - }, - { - "name": "aggregations", - "in": "query", - "description": "A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value", - "schema": { - "type": "string" - } - }, - { - "name": "time", - "in": "query", - "description": "The time filter that limits the data being returned to a specific date range.", - "schema": { - "type": "string" - } - }, - { - "name": "offset", - "in": "query", - "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", - "schema": { - "type": "string" - } - }, - { - "name": "mode", - "in": "query", - "description": "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.", - "schema": { - "type": "string" - } } ], "responses": { @@ -535,181 +637,861 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CountResult" + "$ref": "#/components/schemas/WebHook" } } } }, - "400": { - "description": "Invalid filter." + "404": { + "description": "The web hook could not be found." } } } }, - "/api/v2/projects/{projectId}/events/count": { - "get": { + "/api/v2/webhooks": { + "post": { "tags": [ - "Event" + "WebHook" ], - "summary": "Count by project", - "parameters": [ - { - "name": "projectId", - "in": "path", - "description": "The identifier of the project.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "filter", - "in": "query", - "description": "A filter that controls what data is returned from the server.", - "schema": { - "type": "string" - } - }, - { - "name": "aggregations", - "in": "query", - "description": "A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value", - "schema": { - "type": "string" - } - }, - { - "name": "time", - "in": "query", - "description": "The time filter that limits the data being returned to a specific date range.", - "schema": { - "type": "string" - } - }, - { - "name": "offset", - "in": "query", - "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", - "schema": { - "type": "string" + "summary": "Create", + "requestBody": { + "description": "The web hook.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewWebHook" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NewWebHook" + } } }, - { - "name": "mode", - "in": "query", - "description": "If mode is set to stack_new, then additional filters will be added.", - "schema": { - "type": "string" - } - } - ], + "required": true + }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CountResult" + "$ref": "#/components/schemas/WebHook" } } } }, "400": { - "description": "Invalid filter." + "description": "An error occurred while creating the web hook." + }, + "409": { + "description": "The web hook already exists." } } } }, - "/api/v2/events/{id}": { - "get": { + "/api/v2/webhooks/{ids}": { + "delete": { "tags": [ - "Event" + "WebHook" ], - "summary": "Get by id", - "operationId": "GetPersistentEventById", + "summary": "Remove", "parameters": [ { - "name": "id", + "name": "ids", "in": "path", - "description": "The identifier of the event.", + "description": "A comma-delimited list of web hook identifiers.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "time", - "in": "query", - "description": "The time filter that limits the data being returned to a specific date range.", - "schema": { - "type": "string" - } - }, - { - "name": "offset", - "in": "query", - "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", - "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } } ], "responses": { - "200": { - "description": "OK", + "202": { + "description": "Accepted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PersistentEvent" + "$ref": "#/components/schemas/WorkInProgressResult" } } } }, + "400": { + "description": "One or more validation errors occurred." + }, "404": { - "description": "The event occurrence could not be found." + "description": "One or more web hooks were not found." }, - "426": { - "description": "Unable to view event occurrence due to plan limits." + "500": { + "description": "An error occurred while deleting one or more web hooks." } } } }, - "/api/v2/events": { - "get": { + "/api/v2/auth/login": { + "post": { "tags": [ - "Event" + "Auth" ], - "summary": "Get all", - "parameters": [ - { - "name": "filter", - "in": "query", - "description": "A filter that controls what data is returned from the server.", - "schema": { - "type": "string" + "summary": "Login", + "description": "Log in with your email address and password to generate a token scoped with your users roles.\n\n```{ \"email\": \"noreply@exceptionless.io\", \"password\": \"exceptionless\" }```\n\nThis token can then be used to access the api. You can use this token in the header (bearer authentication)\nor append it onto the query string: ?access_token=MY_TOKEN\n\nPlease note that you can also use this token on the documentation site by placing it in the\nheaders api_key input box.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Login" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Login" + } } }, - { - "name": "sort", - "in": "query", - "description": "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.", - "schema": { - "type": "string" + "required": true + }, + "responses": { + "200": { + "description": "User Authentication Token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResult" + } + } } }, - { - "name": "time", - "in": "query", - "description": "The time filter that limits the data being returned to a specific date range.", - "schema": { - "type": "string" + "401": { + "description": "Login failed" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v2/auth/logout": { + "get": { + "tags": [ + "Auth" + ], + "summary": "Logout the current user and remove the current access token", + "responses": { + "200": { + "description": "User successfully logged-out", + "content": { + "application/json": { } } }, - { - "name": "offset", - "in": "query", + "401": { + "description": "User not logged in" + }, + "403": { + "description": "Current action is not supported with user access token" + } + } + } + }, + "/api/v2/auth/signup": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Sign up", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Signup" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Signup" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User Authentication Token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResult" + } + } + } + }, + "401": { + "description": "Sign-up failed" + }, + "403": { + "description": "Account Creation is currently disabled" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v2/auth/github": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Sign in with GitHub", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalAuthInfo" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ExternalAuthInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User Authentication Token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResult" + } + } + } + }, + "403": { + "description": "Account Creation is currently disabled" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v2/auth/google": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Sign in with Google", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalAuthInfo" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ExternalAuthInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User Authentication Token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResult" + } + } + } + }, + "403": { + "description": "Account Creation is currently disabled" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v2/auth/facebook": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Sign in with Facebook", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalAuthInfo" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ExternalAuthInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User Authentication Token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResult" + } + } + } + }, + "403": { + "description": "Account Creation is currently disabled" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v2/auth/live": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Sign in with Microsoft", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalAuthInfo" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ExternalAuthInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User Authentication Token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResult" + } + } + } + }, + "403": { + "description": "Account Creation is currently disabled" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v2/auth/unlink/{providerName}": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Removes an external login provider from the account", + "parameters": [ + { + "name": "providerName", + "in": "path", + "description": "The provider name.", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "description": "The provider user id.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User Authentication Token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResult" + } + } + } + }, + "400": { + "description": "Invalid provider name." + } + } + } + }, + "/api/v2/auth/change-password": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Change password", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangePasswordModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ChangePasswordModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User Authentication Token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResult" + } + } + } + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v2/auth/forgot-password/{email}": { + "get": { + "tags": [ + "Auth" + ], + "summary": "Forgot password", + "parameters": [ + { + "name": "email", + "in": "path", + "description": "The email address.", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Forgot password email was sent.", + "content": { + "application/json": { } + } + }, + "400": { + "description": "Invalid email address." + } + } + } + }, + "/api/v2/auth/reset-password": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Reset password", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResetPasswordModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ResetPasswordModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password reset email was sent.", + "content": { + "application/json": { } + } + }, + "422": { + "description": "Invalid reset password model." + } + } + } + }, + "/api/v2/auth/cancel-reset-password/{token}": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Cancel reset password", + "parameters": [ + { + "name": "token", + "in": "path", + "description": "The password reset token.", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Password reset email was cancelled.", + "content": { + "application/json": { } + } + }, + "400": { + "description": "Invalid password reset token." + } + } + } + }, + "/api/v2/events/count": { + "get": { + "tags": [ + "Event" + ], + "summary": "Count", + "parameters": [ + { + "name": "filter", + "in": "query", + "description": "A filter that controls what data is returned from the server.", + "schema": { + "type": "string" + } + }, + { + "name": "aggregations", + "in": "query", + "description": "A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value", + "schema": { + "type": "string" + } + }, + { + "name": "time", + "in": "query", + "description": "The time filter that limits the data being returned to a specific date range.", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "schema": { + "type": "string" + } + }, + { + "name": "mode", + "in": "query", + "description": "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountResult" + } + } + } + }, + "400": { + "description": "Invalid filter." + } + } + } + }, + "/api/v2/organizations/{organizationId}/events/count": { + "get": { + "tags": [ + "Event" + ], + "summary": "Count by organization", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "description": "The identifier of the organization.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + }, + { + "name": "filter", + "in": "query", + "description": "A filter that controls what data is returned from the server.", + "schema": { + "type": "string" + } + }, + { + "name": "aggregations", + "in": "query", + "description": "A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value", + "schema": { + "type": "string" + } + }, + { + "name": "time", + "in": "query", + "description": "The time filter that limits the data being returned to a specific date range.", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "schema": { + "type": "string" + } + }, + { + "name": "mode", + "in": "query", + "description": "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountResult" + } + } + } + }, + "400": { + "description": "Invalid filter." + } + } + } + }, + "/api/v2/projects/{projectId}/events/count": { + "get": { + "tags": [ + "Event" + ], + "summary": "Count by project", + "parameters": [ + { + "name": "projectId", + "in": "path", + "description": "The identifier of the project.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + }, + { + "name": "filter", + "in": "query", + "description": "A filter that controls what data is returned from the server.", + "schema": { + "type": "string" + } + }, + { + "name": "aggregations", + "in": "query", + "description": "A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value", + "schema": { + "type": "string" + } + }, + { + "name": "time", + "in": "query", + "description": "The time filter that limits the data being returned to a specific date range.", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "schema": { + "type": "string" + } + }, + { + "name": "mode", + "in": "query", + "description": "If mode is set to stack_new, then additional filters will be added.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountResult" + } + } + } + }, + "400": { + "description": "Invalid filter." + } + } + } + }, + "/api/v2/events/{id}": { + "get": { + "tags": [ + "Event" + ], + "summary": "Get by id", + "operationId": "GetPersistentEventById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The identifier of the event.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + }, + { + "name": "time", + "in": "query", + "description": "The time filter that limits the data being returned to a specific date range.", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersistentEvent" + } + } + } + }, + "404": { + "description": "The event occurrence could not be found." + }, + "426": { + "description": "Unable to view event occurrence due to plan limits." + } + } + } + }, + "/api/v2/events": { + "get": { + "tags": [ + "Event" + ], + "summary": "Get all", + "parameters": [ + { + "name": "filter", + "in": "query", + "description": "A filter that controls what data is returned from the server.", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "description": "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.", + "schema": { + "type": "string" + } + }, + { + "name": "time", + "in": "query", + "description": "The time filter that limits the data being returned to a specific date range.", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", "schema": { "type": "string" @@ -728,7 +1510,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -737,7 +1523,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -786,7 +1576,7 @@ "Event" ], "summary": "Submit event by POST", - "description": "You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it\nwe will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON\nobject into the events data collection.\n \nYou can also post a multi-line string. We automatically split strings by the \\n character and create a new log event for every line.\n \nSimple event:\n```\n{ \"message\": \"Exceptionless is amazing!\" }\n```\n \nSimple log event with user identity:\n```\n{\n \"type\": \"log\",\n \"message\": \"Exceptionless is amazing!\",\n \"date\":\"2030-01-01T12:00:00.0000000-05:00\",\n \"@user\":{ \"identity\":\"123456789\", \"name\": \"Test User\" }\n}\n```\n \nMultiple events from string content:\n```\nExceptionless is amazing!\nExceptionless is really amazing!\n```\n \nSimple error:\n```\n{\n \"type\": \"error\",\n \"date\":\"2030-01-01T12:00:00.0000000-05:00\",\n \"@simple_error\": {\n \"message\": \"Simple Exception\",\n \"type\": \"System.Exception\",\n \"stack_trace\": \" at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77\"\n }\n}\n```", + "description": " You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it\n we will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON\n object into the events data collection.\n\n You can also post a multi-line string. We automatically split strings by the \\n character and create a new log event for every line.\n\n Simple event:\n ```{ \"message\": \"Exceptionless is amazing!\" }```\n\n Simple log event with user identity:\n ```{\n \"type\": \"log\",\n \"message\": \"Exceptionless is amazing!\",\n \"date\":\"2030-01-01T12:00:00.0000000-05:00\",\n \"@user\":{ \"identity\":\"123456789\", \"name\": \"Test User\" }\n}```\n\n Multiple events from string content:\n ```Exceptionless is amazing!\nExceptionless is really amazing!```\n\n Simple error:\n ```{\n \"type\": \"error\",\n \"date\":\"2030-01-01T12:00:00.0000000-05:00\",\n \"@simple_error\": {\n \"message\": \"Simple Exception\",\n \"type\": \"System.Exception\",\n \"stack_trace\": \" at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77\"\n }\n}```", "parameters": [ { "name": "userAgent", @@ -816,7 +1606,10 @@ }, "responses": { "202": { - "description": "Accepted" + "description": "Accepted", + "content": { + "application/json": { } + } }, "400": { "description": "No project id specified and no default project was found." @@ -889,7 +1682,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -898,7 +1695,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -1008,7 +1809,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -1017,7 +1822,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -1069,7 +1878,7 @@ "Event" ], "summary": "Submit event by POST for a specific project", - "description": "You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it\nwe will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON\nobject into the events data collection.\n \nYou can also post a multi-line string. We automatically split strings by the \\n character and create a new log event for every line.\n \nSimple event:\n```\n{ \"message\": \"Exceptionless is amazing!\" }\n```\n \nSimple log event with user identity:\n```\n{\n \"type\": \"log\",\n \"message\": \"Exceptionless is amazing!\",\n \"date\":\"2030-01-01T12:00:00.0000000-05:00\",\n \"@user\":{ \"identity\":\"123456789\", \"name\": \"Test User\" }\n}\n```\n \nMultiple events from string content:\n```\nExceptionless is amazing!\nExceptionless is really amazing!\n```\n \nSimple error:\n```\n{\n \"type\": \"error\",\n \"date\":\"2030-01-01T12:00:00.0000000-05:00\",\n \"@simple_error\": {\n \"message\": \"Simple Exception\",\n \"type\": \"System.Exception\",\n \"stack_trace\": \" at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77\"\n }\n}\n```", + "description": " You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it\n we will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON\n object into the events data collection.\n\n You can also post a multi-line string. We automatically split strings by the \\n character and create a new log event for every line.\n\n Simple event:\n ```{ \"message\": \"Exceptionless is amazing!\" }```\n\n Simple log event with user identity:\n ```{\n \"type\": \"log\",\n \"message\": \"Exceptionless is amazing!\",\n \"date\":\"2030-01-01T12:00:00.0000000-05:00\",\n \"@user\":{ \"identity\":\"123456789\", \"name\": \"Test User\" }\n}```\n\n Multiple events from string content:\n ```Exceptionless is amazing!\nExceptionless is really amazing!```\n\n Simple error:\n ```{\n \"type\": \"error\",\n \"date\":\"2030-01-01T12:00:00.0000000-05:00\",\n \"@simple_error\": {\n \"message\": \"Simple Exception\",\n \"type\": \"System.Exception\",\n \"stack_trace\": \" at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77\"\n }\n}```", "parameters": [ { "name": "projectId", @@ -1109,7 +1918,10 @@ }, "responses": { "202": { - "description": "Accepted" + "description": "Accepted", + "content": { + "application/json": { } + } }, "400": { "description": "No project id specified and no default project was found." @@ -1182,7 +1994,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -1191,7 +2007,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -1277,7 +2097,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -1286,7 +2110,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -1379,7 +2207,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -1388,7 +2220,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -1498,7 +2334,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -1507,7 +2347,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -1624,7 +2468,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -1633,7 +2481,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -1733,7 +2585,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -1742,7 +2598,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -1846,7 +2706,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -1855,7 +2719,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -1965,7 +2833,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -1974,7 +2846,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -2042,18 +2918,27 @@ } ], "requestBody": { - "description": "The user description.", + "description": "The identifier of the project.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserDescription" } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UserDescription" + } } - } + }, + "required": true }, "responses": { "202": { - "description": "Accepted" + "description": "Accepted", + "content": { + "application/json": { } + } }, "400": { "description": "Description must be specified." @@ -2100,56 +2985,281 @@ "schema": { "$ref": "#/components/schemas/UserDescription" } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UserDescription" + } } - } + }, + "required": true }, "responses": { "202": { - "description": "Accepted" + "description": "Accepted", + "content": { + "application/json": { } + } }, "400": { "description": "Description must be specified." }, - "404": { - "description": "The event occurrence with the specified reference id could not be found." - } - } - } - }, - "/api/v2/events/session/heartbeat": { - "get": { - "tags": [ - "Event" - ], - "summary": "Submit heartbeat", - "parameters": [ + "404": { + "description": "The event occurrence with the specified reference id could not be found." + } + } + } + }, + "/api/v1/error/{id}": { + "patch": { + "tags": [ + "Event" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEvent" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateEvent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { } + } + } + } + } + }, + "/api/v2/events/session/heartbeat": { + "get": { + "tags": [ + "Event" + ], + "summary": "Submit heartbeat", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "The session id or user id.", + "schema": { + "type": "string" + } + }, + { + "name": "close", + "in": "query", + "description": "If true, the session will be closed.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { } + } + }, + "400": { + "description": "No project id specified and no default project was found." + }, + "404": { + "description": "No project was found." + } + } + } + }, + "/api/v1/events/submit": { + "get": { + "tags": [ + "Event" + ], + "parameters": [ + { + "name": "userAgent", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "parameters", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/KeyValuePairOfstringAndStringValues" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { } + } + } + } + } + }, + "/api/v1/events/submit/{type}": { + "get": { + "tags": [ + "Event" + ], + "parameters": [ + { + "name": "type", + "in": "path", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, + { + "name": "userAgent", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "parameters", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/KeyValuePairOfstringAndStringValues" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { } + } + } + } + } + }, + "/api/v1/projects/{projectId}/events/submit": { + "get": { + "tags": [ + "Event" + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + }, + { + "name": "userAgent", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "parameters", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/KeyValuePairOfstringAndStringValues" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { } + } + } + } + } + }, + "/api/v1/projects/{projectId}/events/submit/{type}": { + "get": { + "tags": [ + "Event" + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + }, + { + "name": "type", + "in": "path", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, { - "name": "id", - "in": "query", - "description": "The session id or user id.", + "name": "userAgent", + "in": "header", "schema": { "type": "string" } }, { - "name": "close", + "name": "parameters", "in": "query", - "description": "If true, the session will be closed.", "schema": { - "type": "boolean", - "default": false + "type": "array", + "items": { + "$ref": "#/components/schemas/KeyValuePairOfstringAndStringValues" + } } } ], "responses": { "200": { - "description": "OK" - }, - "400": { - "description": "No project id specified and no default project was found." - }, - "404": { - "description": "No project was found." + "description": "OK", + "content": { + "application/json": { } + } } } } @@ -2160,7 +3270,7 @@ "Event" ], "summary": "Submit event by GET", - "description": "You can submit an event using an HTTP GET and query string parameters. Any unknown query string parameters will be added to the extended data of the event.\n \nFeature usage named build with a duration of 10:\n```/events/submit?access_token=YOUR_API_KEY&type=usage&source=build&value=10```\n \nLog with message, geo and extended data\n```/events/submit?access_token=YOUR_API_KEY&type=log&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```", + "description": "You can submit an event using an HTTP GET and query string parameters. Any unknown query string parameters will be added to the extended data of the event.\n\nFeature usage named build with a duration of 10:\n```/events/submit?access_token=YOUR_API_KEY&type=usage&source=build&value=10```\n\nLog with message, geo and extended data\n```/events/submit?access_token=YOUR_API_KEY&type=log&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```", "parameters": [ { "name": "type", @@ -2207,7 +3317,11 @@ "in": "query", "description": "The number of duplicated events.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -2216,7 +3330,11 @@ "in": "query", "description": "The value of the event if any.", "schema": { - "type": "number", + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], "format": "double" } }, @@ -2267,14 +3385,17 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/StringStringValuesKeyValuePair" + "$ref": "#/components/schemas/KeyValuePairOfstringAndStringValues" } } } ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { } + } }, "400": { "description": "No project id specified and no default project was found." @@ -2291,7 +3412,7 @@ "Event" ], "summary": "Submit event type by GET", - "description": "You can submit an event using an HTTP GET and query string parameters.\n \nFeature usage event named build with a value of 10:\n```/events/submit/usage?access_token=YOUR_API_KEY&source=build&value=10```\n \nLog event with message, geo and extended data\n```/events/submit/log?access_token=YOUR_API_KEY&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```", + "description": "You can submit an event using an HTTP GET and query string parameters.\n\nFeature usage event named build with a value of 10:\n```/events/submit/usage?access_token=YOUR_API_KEY&source=build&value=10```\n\nLog event with message, geo and extended data\n```/events/submit/log?access_token=YOUR_API_KEY&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```", "parameters": [ { "name": "type", @@ -2340,7 +3461,11 @@ "in": "query", "description": "The number of duplicated events.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -2349,7 +3474,11 @@ "in": "query", "description": "The value of the event if any.", "schema": { - "type": "number", + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], "format": "double" } }, @@ -2400,14 +3529,17 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/StringStringValuesKeyValuePair" + "$ref": "#/components/schemas/KeyValuePairOfstringAndStringValues" } } } ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { } + } }, "400": { "description": "No project id specified and no default project was found." @@ -2424,7 +3556,7 @@ "Event" ], "summary": "Submit event type by GET for a specific project", - "description": "You can submit an event using an HTTP GET and query string parameters.\n \nFeature usage named build with a duration of 10:\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=usage&source=build&value=10```\n \nLog with message, geo and extended data\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=log&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```", + "description": "You can submit an event using an HTTP GET and query string parameters.\n\nFeature usage named build with a duration of 10:\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=usage&source=build&value=10```\n\nLog with message, geo and extended data\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=log&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```", "parameters": [ { "name": "projectId", @@ -2473,7 +3605,11 @@ "in": "query", "description": "The number of duplicated events.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -2482,7 +3618,11 @@ "in": "query", "description": "The value of the event if any.", "schema": { - "type": "number", + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], "format": "double" } }, @@ -2533,14 +3673,17 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/StringStringValuesKeyValuePair" + "$ref": "#/components/schemas/KeyValuePairOfstringAndStringValues" } } } ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { } + } }, "400": { "description": "No project id specified and no default project was found." @@ -2557,7 +3700,7 @@ "Event" ], "summary": "Submit event type by GET for a specific project", - "description": "You can submit an event using an HTTP GET and query string parameters.\n \nFeature usage named build with a duration of 10:\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=usage&source=build&value=10```\n \nLog with message, geo and extended data\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=log&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```", + "description": "You can submit an event using an HTTP GET and query string parameters.\n\nFeature usage named build with a duration of 10:\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=usage&source=build&value=10```\n\nLog with message, geo and extended data\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=log&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```", "parameters": [ { "name": "projectId", @@ -2616,7 +3759,11 @@ "in": "query", "description": "The number of duplicated events.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } }, @@ -2625,7 +3772,11 @@ "in": "query", "description": "The value of the event if any.", "schema": { - "type": "number", + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], "format": "double" } }, @@ -2676,423 +3827,212 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/StringStringValuesKeyValuePair" + "$ref": "#/components/schemas/KeyValuePairOfstringAndStringValues" } } } ], "responses": { "200": { - "description": "OK" - }, - "400": { - "description": "No project id specified and no default project was found." - }, - "404": { - "description": "No project was found." - } - } - } - }, - "/api/v2/events/{ids}": { - "delete": { - "tags": [ - "Event" - ], - "summary": "Remove", - "parameters": [ - { - "name": "ids", - "in": "path", - "description": "A comma-delimited list of event identifiers.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", - "type": "string" - } - } - ], - "responses": { - "202": { - "description": "Accepted", + "description": "OK", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkInProgressResult" - } - } + "application/json": { } } }, "400": { - "description": "One or more validation errors occurred." + "description": "No project id specified and no default project was found." }, "404": { - "description": "One or more event occurrences were not found." - }, - "500": { - "description": "An error occurred while deleting one or more event occurrences." + "description": "No project was found." } } } }, - "/api/v2/organizations": { - "get": { - "tags": [ - "Organization" - ], - "summary": "Get all", - "parameters": [ - { - "name": "mode", - "in": "query", - "description": "If no mode is set then a lightweight organization object will be returned. If the mode is set to stats than the fully populated object will be returned.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ViewOrganization" - } - } - } - } - } - } - }, + "/api/v1/error": { "post": { "tags": [ - "Organization" - ], - "summary": "Create", - "requestBody": { - "description": "The organization.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewOrganization" - } - } - } - }, - "responses": { - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewOrganization" - } - } - } - }, - "400": { - "description": "An error occurred while creating the organization." - }, - "409": { - "description": "The organization already exists." - } - } - } - }, - "/api/v2/organizations/{id}": { - "get": { - "tags": [ - "Organization" - ], - "summary": "Get by id", - "operationId": "GetOrganizationById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The identifier of the organization.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "mode", - "in": "query", - "description": "If no mode is set then the a lightweight organization object will be returned. If the mode is set to stats than the fully populated object will be returned.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewOrganization" - } - } - } - }, - "404": { - "description": "The organization could not be found." - } - } - }, - "patch": { - "tags": [ - "Organization" + "Event" ], - "summary": "Update", "parameters": [ { - "name": "id", - "in": "path", - "description": "The identifier of the organization.", - "required": true, + "name": "userAgent", + "in": "header", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } } ], "requestBody": { - "description": "The changes", "content": { "application/json": { "schema": { - "description": "A class the tracks changes (i.e. the Delta) for a particular ." - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewOrganization" - } + "type": "string", + "example": "" } - } - }, - "400": { - "description": "An error occurred while updating the organization." - }, - "404": { - "description": "The organization could not be found." - } - } - }, - "put": { - "tags": [ - "Organization" - ], - "summary": "Update", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The identifier of the organization.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - } - ], - "requestBody": { - "description": "The changes", - "content": { - "application/json": { + }, + "text/plain": { "schema": { - "description": "A class the tracks changes (i.e. the Delta) for a particular ." + "type": "string", + "example": "" } } - } + }, + "required": true }, "responses": { "200": { "description": "OK", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewOrganization" - } - } + "application/json": { } } - }, - "400": { - "description": "An error occurred while updating the organization." - }, - "404": { - "description": "The organization could not be found." } } } }, - "/api/v2/organizations/{ids}": { - "delete": { + "/api/v1/events": { + "post": { "tags": [ - "Organization" + "Event" ], - "summary": "Remove", "parameters": [ { - "name": "ids", - "in": "path", - "description": "A comma-delimited list of organization identifiers.", - "required": true, + "name": "userAgent", + "in": "header", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string", + "example": "" + } + }, + "text/plain": { + "schema": { + "type": "string", + "example": "" + } + } + }, + "required": true + }, "responses": { "202": { "description": "Accepted", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkInProgressResult" - } - } + "application/json": { } } - }, - "400": { - "description": "One or more validation errors occurred." - }, - "404": { - "description": "One or more organizations were not found." - }, - "500": { - "description": "An error occurred while deleting one or more organizations." } } } }, - "/api/v2/organizations/invoice/{id}": { - "get": { + "/api/v1/projects/{projectId}/events": { + "post": { "tags": [ - "Organization" + "Event" ], - "summary": "Get invoice", "parameters": [ { - "name": "id", + "name": "projectId", "in": "path", - "description": "The identifier of the invoice.", "required": true, "schema": { - "minLength": 10, + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + }, + { + "name": "userAgent", + "in": "header", + "schema": { "type": "string" } } ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Invoice" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string", + "example": "" + } + }, + "text/plain": { + "schema": { + "type": "string", + "example": "" } } }, - "404": { - "description": "The invoice was not found." + "required": true + }, + "responses": { + "202": { + "description": "Accepted", + "content": { + "application/json": { } + } } } } }, - "/api/v2/organizations/{id}/invoices": { - "get": { + "/api/v2/events/{ids}": { + "delete": { "tags": [ - "Organization" + "Event" ], - "summary": "Get invoices", + "summary": "Remove", "parameters": [ { - "name": "id", + "name": "ids", "in": "path", - "description": "The identifier of the organization.", + "description": "A comma-delimited list of event identifiers.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "before", - "in": "query", - "description": "A cursor for use in pagination. before is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_bar, your subsequent call can include before=obj_bar in order to fetch the previous page of the list.", - "schema": { - "type": "string" - } - }, - { - "name": "after", - "in": "query", - "description": "A cursor for use in pagination. after is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.", - "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } - }, - { - "name": "limit", - "in": "query", - "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", - "schema": { - "type": "integer", - "format": "int32", - "default": 12 - } } ], "responses": { - "200": { - "description": "OK", + "202": { + "description": "Accepted", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/InvoiceGridModel" - } + "$ref": "#/components/schemas/WorkInProgressResult" } } } }, + "400": { + "description": "One or more validation errors occurred." + }, "404": { - "description": "The organization was not found." + "description": "One or more event occurrences were not found." + }, + "500": { + "description": "An error occurred while deleting one or more event occurrences." } } } }, - "/api/v2/organizations/{id}/plans": { + "/api/v2/organizations": { "get": { "tags": [ "Organization" ], - "summary": "Get plans", - "description": "Gets available plans for a specific organization.", + "summary": "Get all", "parameters": [ { - "name": "id", - "in": "path", - "description": "The identifier of the organization.", - "required": true, + "name": "mode", + "in": "query", + "description": "If no mode is set then a lightweight organization object will be returned. If the mode is set to stats than the fully populated object will be returned.", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } } @@ -3105,92 +4045,62 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/BillingPlan" + "$ref": "#/components/schemas/ViewOrganization" } } } } - }, - "404": { - "description": "The organization was not found." } } - } - }, - "/api/v2/organizations/{id}/change-plan": { + }, "post": { "tags": [ "Organization" ], - "summary": "Change plan", - "description": "Upgrades or downgrades the organizations plan.", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The identifier of the organization.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "planId", - "in": "query", - "description": "The identifier of the plan.", - "schema": { - "type": "string" - } - }, - { - "name": "stripeToken", - "in": "query", - "description": "The token returned from the stripe service.", - "schema": { - "type": "string" - } - }, - { - "name": "last4", - "in": "query", - "description": "The last four numbers of the card.", - "schema": { - "type": "string" + "summary": "Create", + "requestBody": { + "description": "The organization.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewOrganization" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NewOrganization" + } } }, - { - "name": "couponId", - "in": "query", - "description": "The coupon id.", - "schema": { - "type": "string" - } - } - ], + "required": true + }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChangePlanResult" + "$ref": "#/components/schemas/ViewOrganization" } } } }, - "404": { - "description": "The organization was not found." + "400": { + "description": "An error occurred while creating the organization." + }, + "409": { + "description": "The organization already exists." } } } }, - "/api/v2/organizations/{id}/users/{email}": { - "post": { + "/api/v2/organizations/{id}": { + "get": { "tags": [ "Organization" ], - "summary": "Add user", + "summary": "Get by id", + "operationId": "GetOrganizationById", "parameters": [ { "name": "id", @@ -3203,12 +4113,10 @@ } }, { - "name": "email", - "in": "path", - "description": "The email address of the user you wish to add to your organization.", - "required": true, + "name": "mode", + "in": "query", + "description": "If no mode is set then the a lightweight organization object will be returned. If the mode is set to stats than the fully populated object will be returned.", "schema": { - "minLength": 1, "type": "string" } } @@ -3219,24 +4127,21 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/ViewOrganization" } } } }, "404": { - "description": "The organization was not found." - }, - "426": { - "description": "Please upgrade your plan to add an additional user." + "description": "The organization could not be found." } } }, - "delete": { + "patch": { "tags": [ "Organization" ], - "summary": "Remove user", + "summary": "Update", "parameters": [ { "name": "id", @@ -3247,37 +4152,48 @@ "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - }, - { - "name": "email", - "in": "path", - "description": "The email address of the user you wish to remove from your organization.", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } } ], + "requestBody": { + "description": "The changes", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewOrganization" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NewOrganization" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewOrganization" + } + } + } }, "400": { - "description": "The error occurred while removing the user from your organization" + "description": "An error occurred while updating the organization." }, "404": { - "description": "The organization was not found." + "description": "The organization could not be found." } } - } - }, - "/api/v2/organizations/{id}/data/{key}": { - "post": { + }, + "put": { "tags": [ "Organization" ], - "summary": "Add custom data", + "summary": "Update", "parameters": [ { "name": "id", @@ -3288,134 +4204,151 @@ "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - }, - { - "name": "key", - "in": "path", - "description": "The key name of the data object.", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } } ], "requestBody": { - "description": "Any string value.", + "description": "The changes", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StringValueFromBody" + "$ref": "#/components/schemas/NewOrganization" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NewOrganization" } } - } + }, + "required": true }, "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewOrganization" + } + } + } + }, + "400": { + "description": "An error occurred while updating the organization." }, "404": { - "description": "The organization was not found." + "description": "The organization could not be found." } } - }, + } + }, + "/api/v2/organizations/{ids}": { "delete": { "tags": [ "Organization" ], - "summary": "Remove custom data", + "summary": "Remove", "parameters": [ { - "name": "id", - "in": "path", - "description": "The identifier of the organization.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "key", + "name": "ids", "in": "path", - "description": "The key name of the data object.", + "description": "A comma-delimited list of organization identifiers.", "required": true, "schema": { - "minLength": 1, + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } } ], "responses": { - "200": { - "description": "OK" + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkInProgressResult" + } + } + } + }, + "400": { + "description": "One or more validation errors occurred." }, "404": { - "description": "The organization was not found." + "description": "One or more organizations were not found." + }, + "500": { + "description": "An error occurred while deleting one or more organizations." } } } }, - "/api/v2/organizations/check-name": { + "/api/v2/organizations/invoice/{id}": { "get": { "tags": [ "Organization" ], - "summary": "Check for unique name", + "summary": "Get invoice", "parameters": [ { - "name": "name", - "in": "query", - "description": "The organization name to check.", + "name": "id", + "in": "path", + "description": "The identifier of the invoice.", + "required": true, "schema": { + "minLength": 10, "type": "string" } } ], "responses": { "200": { - "description": "OK" - }, - "201": { - "description": "The organization name is available." + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } }, - "204": { - "description": "The organization name is not available." + "404": { + "description": "The invoice was not found." } } } }, - "/api/v2/projects": { + "/api/v2/organizations/{id}/invoices": { "get": { "tags": [ - "Project" + "Organization" ], - "summary": "Get all", + "summary": "Get invoices", "parameters": [ { - "name": "filter", - "in": "query", - "description": "A filter that controls what data is returned from the server.", + "name": "id", + "in": "path", + "description": "The identifier of the organization.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } }, { - "name": "sort", + "name": "before", "in": "query", - "description": "Controls the sort order that the data is returned in. In this example -created returns the results descending by the created date.", + "description": "A cursor for use in pagination. before is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_bar, your subsequent call can include before=obj_bar in order to fetch the previous page of the list.", "schema": { "type": "string" } }, { - "name": "page", + "name": "after", "in": "query", - "description": "The page parameter is used for pagination. This value must be greater than 0.", + "description": "A cursor for use in pagination. after is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.", "schema": { - "type": "integer", - "format": "int32", - "default": 1 + "type": "string" } }, { @@ -3423,17 +4356,13 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", - "default": 10 - } - }, - { - "name": "mode", - "in": "query", - "description": "If no mode is set then the lightweight project object will be returned. If the mode is set to stats than the fully populated object will be returned.", - "schema": { - "type": "string" + "default": 12 } } ], @@ -3445,58 +4374,67 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ViewProject" + "$ref": "#/components/schemas/InvoiceGridModel" } } } } + }, + "404": { + "description": "The organization was not found." } } - }, - "post": { + } + }, + "/api/v2/organizations/{id}/plans": { + "get": { "tags": [ - "Project" + "Organization" ], - "summary": "Create", - "requestBody": { - "description": "The project.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewProject" - } + "summary": "Get plans", + "description": "Gets available plans for a specific organization.", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The identifier of the organization.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ViewProject" + "type": "array", + "items": { + "$ref": "#/components/schemas/BillingPlan" + } } } } - }, - "400": { - "description": "An error occurred while creating the project." - }, - "409": { - "description": "The project already exists." + }, + "404": { + "description": "The organization was not found." } } } }, - "/api/v2/organizations/{organizationId}/projects": { - "get": { + "/api/v2/organizations/{id}/change-plan": { + "post": { "tags": [ - "Project" + "Organization" ], - "summary": "Get all", + "summary": "Change plan", + "description": "Upgrades or downgrades the organizations plan.", "parameters": [ { - "name": "organizationId", + "name": "id", "in": "path", "description": "The identifier of the organization.", "required": true, @@ -3506,45 +4444,33 @@ } }, { - "name": "filter", + "name": "planId", "in": "query", - "description": "A filter that controls what data is returned from the server.", + "description": "The identifier of the plan.", "schema": { "type": "string" } }, { - "name": "sort", + "name": "stripeToken", "in": "query", - "description": "Controls the sort order that the data is returned in. In this example -created returns the results descending by the created date.", + "description": "The token returned from the stripe service.", "schema": { "type": "string" } }, { - "name": "page", - "in": "query", - "description": "The page parameter is used for pagination. This value must be greater than 0.", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "limit", + "name": "last4", "in": "query", - "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", + "description": "The last four numbers of the card.", "schema": { - "type": "integer", - "format": "int32", - "default": 10 + "type": "string" } }, { - "name": "mode", + "name": "couponId", "in": "query", - "description": "If no mode is set then the lightweight project object will be returned. If the mode is set to stats than the fully populated object will be returned.", + "description": "The coupon id.", "schema": { "type": "string" } @@ -3556,32 +4482,28 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ViewProject" - } + "$ref": "#/components/schemas/ChangePlanResult" } } } }, "404": { - "description": "The organization could not be found." + "description": "The organization was not found." } } } }, - "/api/v2/projects/{id}": { - "get": { + "/api/v2/organizations/{id}/users/{email}": { + "post": { "tags": [ - "Project" + "Organization" ], - "summary": "Get by id", - "operationId": "GetProjectById", + "summary": "Add user", "parameters": [ { "name": "id", "in": "path", - "description": "The identifier of the project.", + "description": "The identifier of the organization.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", @@ -3589,10 +4511,12 @@ } }, { - "name": "mode", - "in": "query", - "description": "If no mode is set then the lightweight project object will be returned. If the mode is set to stats than the fully populated object will be returned.", + "name": "email", + "in": "path", + "description": "The email address of the user you wish to add to your organization.", + "required": true, "schema": { + "minLength": 1, "type": "string" } } @@ -3603,164 +4527,171 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ViewProject" + "$ref": "#/components/schemas/User" } } } }, "404": { - "description": "The project could not be found." + "description": "The organization was not found." + }, + "426": { + "description": "Please upgrade your plan to add an additional user." } } }, - "patch": { + "delete": { "tags": [ - "Project" + "Organization" ], - "summary": "Update", + "summary": "Remove user", "parameters": [ { "name": "id", "in": "path", - "description": "The identifier of the project.", + "description": "The identifier of the organization.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - } - ], - "requestBody": { - "description": "The changes", - "content": { - "application/json": { - "schema": { - "description": "A class the tracks changes (i.e. the Delta) for a particular ." - } + }, + { + "name": "email", + "in": "path", + "description": "The email address of the user you wish to remove from your organization.", + "required": true, + "schema": { + "minLength": 1, + "type": "string" } } - }, + ], "responses": { "200": { "description": "OK", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewProject" - } - } + "application/json": { } } }, "400": { - "description": "An error occurred while updating the project." + "description": "The error occurred while removing the user from your organization" }, "404": { - "description": "The project could not be found." + "description": "The organization was not found." } } - }, - "put": { + } + }, + "/api/v2/organizations/{id}/data/{key}": { + "post": { "tags": [ - "Project" + "Organization" ], - "summary": "Update", + "summary": "Add custom data", "parameters": [ { "name": "id", "in": "path", - "description": "The identifier of the project.", + "description": "The identifier of the organization.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } + }, + { + "name": "key", + "in": "path", + "description": "The key name of the data object.", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } } ], "requestBody": { - "description": "The changes", + "description": "Any string value.", "content": { "application/json": { "schema": { - "description": "A class the tracks changes (i.e. the Delta) for a particular ." + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" } } - } + }, + "required": true }, "responses": { "200": { "description": "OK", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewProject" - } - } + "application/json": { } } }, - "400": { - "description": "An error occurred while updating the project." - }, "404": { - "description": "The project could not be found." + "description": "The organization was not found." } } - } - }, - "/api/v2/projects/{ids}": { + }, "delete": { "tags": [ - "Project" + "Organization" ], - "summary": "Remove", + "summary": "Remove custom data", "parameters": [ { - "name": "ids", + "name": "id", "in": "path", - "description": "A comma-delimited list of project identifiers.", + "description": "The identifier of the organization.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" + } + }, + { + "name": "key", + "in": "path", + "description": "The key name of the data object.", + "required": true, + "schema": { + "minLength": 1, "type": "string" } } ], "responses": { - "202": { - "description": "Accepted", + "200": { + "description": "OK", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkInProgressResult" - } - } + "application/json": { } } }, - "400": { - "description": "One or more validation errors occurred." - }, "404": { - "description": "One or more projects were not found." - }, - "500": { - "description": "An error occurred while deleting one or more projects." + "description": "The organization was not found." } } } }, - "/api/v2/projects/config": { + "/api/v2/organizations/check-name": { "get": { "tags": [ - "Project" + "Organization" ], - "summary": "Get configuration settings", + "summary": "Check for unique name", "parameters": [ { - "name": "v", + "name": "name", "in": "query", - "description": "The client configuration version.", + "description": "The organization name to check.", "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -3768,46 +4699,75 @@ "200": { "description": "OK", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClientConfiguration" - } - } + "application/json": { } } }, - "304": { - "description": "The client configuration version is the current version." + "201": { + "description": "The organization name is available." }, - "404": { - "description": "The project could not be found." + "204": { + "description": "The organization name is not available." } } } }, - "/api/v2/projects/{id}/config": { + "/api/v2/projects": { "get": { "tags": [ "Project" ], - "summary": "Get configuration settings", + "summary": "Get all", "parameters": [ { - "name": "id", - "in": "path", - "description": "The identifier of the project.", - "required": true, + "name": "filter", + "in": "query", + "description": "A filter that controls what data is returned from the server.", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } }, { - "name": "v", + "name": "sort", + "in": "query", + "description": "Controls the sort order that the data is returned in. In this example -created returns the results descending by the created date.", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "The page parameter is used for pagination. This value must be greater than 0.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 10 + } + }, + { + "name": "mode", "in": "query", - "description": "The client configuration version.", + "description": "If no mode is set then the lightweight project object will be returned. If the mode is set to stats than the fully populated object will be returned.", "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -3817,16 +4777,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ClientConfiguration" + "type": "array", + "items": { + "$ref": "#/components/schemas/ViewProject" + } } } } - }, - "304": { - "description": "The client configuration version is the current version." - }, - "404": { - "description": "The project could not be found." } } }, @@ -3834,59 +4791,54 @@ "tags": [ "Project" ], - "summary": "Add configuration value", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The identifier of the project.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "key", - "in": "query", - "description": "The key name of the configuration object.", - "schema": { - "type": "string" - } - } - ], + "summary": "Create", "requestBody": { - "description": "The configuration value.", + "description": "The project.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StringValueFromBody" + "$ref": "#/components/schemas/NewProject" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NewProject" } } - } + }, + "required": true }, "responses": { - "200": { - "description": "OK" + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewProject" + } + } + } }, "400": { - "description": "Invalid configuration value." + "description": "An error occurred while creating the project." }, - "404": { - "description": "The project could not be found." + "409": { + "description": "The project already exists." } } - }, - "delete": { + } + }, + "/api/v2/organizations/{organizationId}/projects": { + "get": { "tags": [ "Project" ], - "summary": "Remove configuration value", + "summary": "Get all", "parameters": [ { - "name": "id", + "name": "organizationId", "in": "path", - "description": "The identifier of the project.", + "description": "The identifier of the organization.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", @@ -3894,68 +4846,85 @@ } }, { - "name": "key", + "name": "filter", "in": "query", - "description": "The key name of the configuration object.", + "description": "A filter that controls what data is returned from the server.", "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "OK" }, - "400": { - "description": "Invalid key value." + { + "name": "sort", + "in": "query", + "description": "Controls the sort order that the data is returned in. In this example -created returns the results descending by the created date.", + "schema": { + "type": "string" + } }, - "404": { - "description": "The project could not be found." - } - } - } - }, - "/api/v2/projects/{id}/reset-data": { - "get": { - "tags": [ - "Project" - ], - "summary": "Reset project data", - "parameters": [ { - "name": "id", - "in": "path", - "description": "The identifier of the project.", - "required": true, + "name": "page", + "in": "query", + "description": "The page parameter is used for pagination. This value must be greater than 0.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 10 + } + }, + { + "name": "mode", + "in": "query", + "description": "If no mode is set then the lightweight project object will be returned. If the mode is set to stats than the fully populated object will be returned.", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } } ], "responses": { - "202": { - "description": "Accepted", + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkInProgressResult" + "type": "array", + "items": { + "$ref": "#/components/schemas/ViewProject" + } } } } }, "404": { - "description": "The project could not be found." + "description": "The organization could not be found." } } } }, - "/api/v2/users/{userId}/projects/{id}/notifications": { + "/api/v2/projects/{id}": { "get": { "tags": [ "Project" ], - "summary": "Get user notification settings", + "summary": "Get by id", + "operationId": "GetProjectById", "parameters": [ { "name": "id", @@ -3968,12 +4937,10 @@ } }, { - "name": "userId", - "in": "path", - "description": "The identifier of the user.", - "required": true, + "name": "mode", + "in": "query", + "description": "If no mode is set then the lightweight project object will be returned. If the mode is set to stats than the fully populated object will be returned.", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } } @@ -3984,7 +4951,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationSettings" + "$ref": "#/components/schemas/ViewProject" } } } @@ -3994,11 +4961,11 @@ } } }, - "put": { + "patch": { "tags": [ "Project" ], - "summary": "Set user notification settings", + "summary": "Update", "parameters": [ { "name": "id", @@ -4009,126 +4976,48 @@ "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - }, - { - "name": "userId", - "in": "path", - "description": "The identifier of the user.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } } ], "requestBody": { - "description": "The notification settings.", + "description": "The changes", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationSettings" + "$ref": "#/components/schemas/UpdateProject" } - } - } - }, - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "The project could not be found." - } - } - }, - "post": { - "tags": [ - "Project" - ], - "summary": "Set user notification settings", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The identifier of the project.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "userId", - "in": "path", - "description": "The identifier of the user.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - } - ], - "requestBody": { - "description": "The notification settings.", - "content": { - "application/json": { + }, + "application/*+json": { "schema": { - "$ref": "#/components/schemas/NotificationSettings" + "$ref": "#/components/schemas/UpdateProject" } } - } + }, + "required": true }, "responses": { "200": { - "description": "OK" - }, - "404": { - "description": "The project could not be found." - } - } - }, - "delete": { - "tags": [ - "Project" - ], - "summary": "Remove user notification settings", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The identifier of the project.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "userId", - "in": "path", - "description": "The identifier of the user.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewProject" + } + } } - } - ], - "responses": { - "200": { - "description": "OK" + }, + "400": { + "description": "An error occurred while updating the project." }, "404": { "description": "The project could not be found." } } - } - }, - "/api/v2/projects/{id}/{integration}/notifications": { + }, "put": { "tags": [ "Project" ], - "summary": "Set an integrations notification settings", + "summary": "Update", "parameters": [ { "name": "id", @@ -4139,133 +5028,165 @@ "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - }, - { - "name": "integration", - "in": "path", - "description": "The identifier of the integration.", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } } ], "requestBody": { - "description": "The notification settings.", + "description": "The changes", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationSettings" + "$ref": "#/components/schemas/UpdateProject" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateProject" } } - } + }, + "required": true }, "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewProject" + } + } + } }, - "404": { - "description": "The project or integration could not be found." + "400": { + "description": "An error occurred while updating the project." }, - "426": { - "description": "Please upgrade your plan to enable integrations." + "404": { + "description": "The project could not be found." } } - }, - "post": { + } + }, + "/api/v2/projects/{ids}": { + "delete": { "tags": [ "Project" ], - "summary": "Set an integrations notification settings", + "summary": "Remove", "parameters": [ { - "name": "id", - "in": "path", - "description": "The identifier of the project.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "integration", + "name": "ids", "in": "path", - "description": "The identifier of the integration.", + "description": "A comma-delimited list of project identifiers.", "required": true, "schema": { - "minLength": 1, + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } } ], - "requestBody": { - "description": "The notification settings.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotificationSettings" + "responses": { + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkInProgressResult" + } } } - } - }, - "responses": { - "200": { - "description": "OK" + }, + "400": { + "description": "One or more validation errors occurred." }, "404": { - "description": "The project or integration could not be found." + "description": "One or more projects were not found." }, - "426": { - "description": "Please upgrade your plan to enable integrations." + "500": { + "description": "An error occurred while deleting one or more projects." } } } }, - "/api/v2/projects/{id}/promotedtabs": { - "put": { + "/api/v1/project/config": { + "get": { "tags": [ "Project" ], - "summary": "Promote tab", "parameters": [ { - "name": "id", - "in": "path", - "description": "The identifier of the project.", - "required": true, + "name": "v", + "in": "query", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" } - }, + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientConfiguration" + } + } + } + } + } + } + }, + "/api/v2/projects/config": { + "get": { + "tags": [ + "Project" + ], + "summary": "Get configuration settings", + "parameters": [ { - "name": "name", + "name": "v", "in": "query", - "description": "The tab name.", + "description": "The client configuration version.", "schema": { - "type": "string" + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" } } ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientConfiguration" + } + } + } }, - "400": { - "description": "Invalid tab name." + "304": { + "description": "The client configuration version is the current version." }, "404": { "description": "The project could not be found." } } - }, - "post": { + } + }, + "/api/v2/projects/{id}/config": { + "get": { "tags": [ "Project" ], - "summary": "Promote tab", + "summary": "Get configuration settings", "parameters": [ { "name": "id", @@ -4278,31 +5199,43 @@ } }, { - "name": "name", + "name": "v", "in": "query", - "description": "The tab name.", + "description": "The client configuration version.", "schema": { - "type": "string" + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" } } ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientConfiguration" + } + } + } }, - "400": { - "description": "Invalid tab name." + "304": { + "description": "The client configuration version is the current version." }, "404": { "description": "The project could not be found." } } }, - "delete": { + "post": { "tags": [ "Project" ], - "summary": "Demote tab", + "summary": "Add configuration value", "parameters": [ { "name": "id", @@ -4315,95 +5248,92 @@ } }, { - "name": "name", + "name": "key", "in": "query", - "description": "The tab name.", + "description": "The key name of the configuration object.", "schema": { "type": "string" } } ], + "requestBody": { + "description": "The configuration value.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { } + } }, "400": { - "description": "Invalid tab name." + "description": "Invalid configuration value." }, "404": { "description": "The project could not be found." } } - } - }, - "/api/v2/projects/check-name": { - "get": { + }, + "delete": { "tags": [ "Project" ], - "summary": "Check for unique name", + "summary": "Remove configuration value", "parameters": [ { - "name": "name", - "in": "query", - "description": "The project name to check.", + "name": "id", + "in": "path", + "description": "The identifier of the project.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - } - ], - "responses": { - "201": { - "description": "The project name is available." }, - "204": { - "description": "The project name is not available." - } - } - } - }, - "/api/v2/organizations/{organizationId}/projects/check-name": { - "get": { - "tags": [ - "Project" - ], - "summary": "Check for unique name", - "parameters": [ { - "name": "name", + "name": "key", "in": "query", - "description": "The project name to check.", - "schema": { - "type": "string" - } - }, - { - "name": "organizationId", - "in": "path", - "description": "If set the check name will be scoped to a specific organization.", - "required": true, + "description": "The key name of the configuration object.", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } } ], "responses": { - "201": { - "description": "The project name is available." + "200": { + "description": "OK", + "content": { + "application/json": { } + } }, - "204": { - "description": "The project name is not available." + "400": { + "description": "Invalid key value." + }, + "404": { + "description": "The project could not be found." } } } }, - "/api/v2/projects/{id}/data": { - "post": { + "/api/v2/projects/{id}/reset-data": { + "get": { "tags": [ "Project" ], - "summary": "Add custom data", + "summary": "Reset project data", "parameters": [ { "name": "id", @@ -4414,43 +5344,31 @@ "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - }, - { - "name": "key", - "in": "query", - "description": "The key name of the data object.", - "schema": { - "type": "string" - } } ], - "requestBody": { - "description": "Any string value.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StringValueFromBody" + "responses": { + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkInProgressResult" + } } } - } - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Invalid key or value." }, "404": { "description": "The project could not be found." } } - }, - "delete": { + } + }, + "/api/v2/users/{userId}/projects/{id}/notifications": { + "get": { "tags": [ "Project" ], - "summary": "Remove custom data", + "summary": "Get user notification settings", "parameters": [ { "name": "id", @@ -4463,39 +5381,42 @@ } }, { - "name": "key", - "in": "query", - "description": "The key name of the data object.", + "name": "userId", + "in": "path", + "description": "The identifier of the user.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } } ], "responses": { "200": { - "description": "OK" - }, - "400": { - "description": "Invalid key or value." + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationSettings" + } + } + } }, "404": { "description": "The project could not be found." } } - } - }, - "/api/v2/stacks/{id}": { - "get": { + }, + "put": { "tags": [ - "Stack" + "Project" ], - "summary": "Get by id", - "operationId": "GetStackById", + "summary": "Set user notification settings", "parameters": [ { "name": "id", "in": "path", - "description": "The identifier of the stack.", + "description": "The identifier of the project.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", @@ -4503,416 +5424,552 @@ } }, { - "name": "offset", - "in": "query", - "description": "The time offset in minutes that controls what data is returned based on the `time` filter. This is used for time zone support.", + "name": "userId", + "in": "path", + "description": "The identifier of the user.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } } ], + "requestBody": { + "description": "The notification settings.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationSettings" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NotificationSettings" + } + } + }, + "required": true + }, "responses": { "200": { "description": "OK", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Stack" - } - } + "application/json": { } } }, "404": { - "description": "The stack could not be found." + "description": "The project could not be found." } } - } - }, - "/api/v2/stacks/{ids}/mark-fixed": { + }, "post": { "tags": [ - "Stack" + "Project" ], - "summary": "Mark fixed", + "summary": "Set user notification settings", "parameters": [ { - "name": "ids", + "name": "id", "in": "path", - "description": "A comma-delimited list of stack identifiers.", + "description": "The identifier of the project.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } }, { - "name": "version", - "in": "query", - "description": "A version number that the stack was fixed in.", + "name": "userId", + "in": "path", + "description": "The identifier of the user.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } } ], + "requestBody": { + "description": "The notification settings.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationSettings" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NotificationSettings" + } + } + }, + "required": true + }, "responses": { - "202": { - "description": "Accepted" + "200": { + "description": "OK", + "content": { + "application/json": { } + } }, "404": { - "description": "One or more stacks could not be found." + "description": "The project could not be found." } } - } - }, - "/api/v2/stacks/{ids}/mark-snoozed": { - "post": { + }, + "delete": { "tags": [ - "Stack" + "Project" ], - "summary": "Mark the selected stacks as snoozed", + "summary": "Remove user notification settings", "parameters": [ { - "name": "ids", + "name": "id", "in": "path", - "description": "A comma-delimited list of stack identifiers.", + "description": "The identifier of the project.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } }, { - "name": "snoozeUntilUtc", - "in": "query", - "description": "A time that the stack should be snoozed until.", + "name": "userId", + "in": "path", + "description": "The identifier of the user.", + "required": true, "schema": { - "type": "string", - "format": "date-time" + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" } } ], "responses": { - "202": { - "description": "Accepted" + "200": { + "description": "OK", + "content": { + "application/json": { } + } }, "404": { - "description": "One or more stacks could not be found." + "description": "The project could not be found." } } } }, - "/api/v2/stacks/{id}/add-link": { - "post": { + "/api/v2/projects/{id}/{integration}/notifications": { + "put": { "tags": [ - "Stack" + "Project" ], - "summary": "Add reference link", + "summary": "Set an integrations notification settings", "parameters": [ { "name": "id", "in": "path", - "description": "The identifier of the stack.", + "description": "The identifier of the project.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } + }, + { + "name": "integration", + "in": "path", + "description": "The identifier of the integration.", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } } ], "requestBody": { - "description": "The reference link.", + "description": "The notification settings.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StringValueFromBody" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NotificationSettings" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NotificationSettings" + } + ] } } } }, "responses": { "200": { - "description": "OK" - }, - "400": { - "description": "Invalid reference link." + "description": "OK", + "content": { + "application/json": { } + } }, "404": { - "description": "The stack could not be found." + "description": "The project or integration could not be found." + }, + "426": { + "description": "Please upgrade your plan to enable integrations." } } - } - }, - "/api/v2/stacks/{id}/remove-link": { + }, "post": { "tags": [ - "Stack" + "Project" ], - "summary": "Remove reference link", + "summary": "Set an integrations notification settings", "parameters": [ { "name": "id", "in": "path", - "description": "The identifier of the stack.", + "description": "The identifier of the project.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } + }, + { + "name": "integration", + "in": "path", + "description": "The identifier of the integration.", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } } ], "requestBody": { - "description": "The reference link.", + "description": "The notification settings.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StringValueFromBody" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NotificationSettings" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NotificationSettings" + } + ] } } } }, "responses": { - "204": { - "description": "The reference link was removed." - }, - "400": { - "description": "Invalid reference link." + "200": { + "description": "OK", + "content": { + "application/json": { } + } }, "404": { - "description": "The stack could not be found." + "description": "The project or integration could not be found." + }, + "426": { + "description": "Please upgrade your plan to enable integrations." } } } }, - "/api/v2/stacks/{ids}/mark-critical": { - "post": { + "/api/v2/projects/{id}/promotedtabs": { + "put": { "tags": [ - "Stack" + "Project" ], - "summary": "Mark future occurrences as critical", + "summary": "Promote tab", "parameters": [ { - "name": "ids", + "name": "id", "in": "path", - "description": "A comma-delimited list of stack identifiers.", + "description": "The identifier of the project.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - } - ], - "responses": { - "200": { - "description": "OK" }, - "404": { - "description": "One or more stacks could not be found." - } - } - }, - "delete": { - "tags": [ - "Stack" - ], - "summary": "Mark future occurrences as not critical", - "parameters": [ { - "name": "ids", - "in": "path", - "description": "A comma-delimited list of stack identifiers.", - "required": true, + "name": "name", + "in": "query", + "description": "The tab name.", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } } ], "responses": { - "204": { - "description": "The stacks were marked as not critical." + "200": { + "description": "OK", + "content": { + "application/json": { } + } + }, + "400": { + "description": "Invalid tab name." }, "404": { - "description": "One or more stacks could not be found." + "description": "The project could not be found." } } - } - }, - "/api/v2/stacks/{ids}/change-status": { + }, "post": { "tags": [ - "Stack" + "Project" ], - "summary": "Change stack status", + "summary": "Promote tab", "parameters": [ { - "name": "ids", + "name": "id", "in": "path", - "description": "A comma-delimited list of stack identifiers.", + "description": "The identifier of the project.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } }, { - "name": "status", + "name": "name", "in": "query", - "description": "The status that the stack should be changed to.", + "description": "The tab name.", "schema": { - "$ref": "#/components/schemas/StackStatus" + "type": "string" } } ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { } + } + }, + "400": { + "description": "Invalid tab name." }, "404": { - "description": "One or more stacks could not be found." + "description": "The project could not be found." } } - } - }, - "/api/v2/stacks/{id}/promote": { - "post": { + }, + "delete": { "tags": [ - "Stack" + "Project" ], - "summary": "Promote to external service", + "summary": "Demote tab", "parameters": [ { "name": "id", "in": "path", - "description": "The identifier of the stack.", + "description": "The identifier of the project.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } + }, + { + "name": "name", + "in": "query", + "description": "The tab name.", + "schema": { + "type": "string" + } } ], "responses": { "200": { - "description": "OK" - }, - "404": { - "description": "The stack could not be found." + "description": "OK", + "content": { + "application/json": { } + } }, - "426": { - "description": "Promote to External is a premium feature used to promote an error stack to an external system." + "400": { + "description": "Invalid tab name." }, - "501": { - "description": "No promoted web hooks are configured for this project." + "404": { + "description": "The project could not be found." } } } }, - "/api/v2/stacks/{ids}": { - "delete": { + "/api/v2/projects/check-name": { + "get": { "tags": [ - "Stack" + "Project" ], - "summary": "Remove", + "summary": "Check for unique name", "parameters": [ { - "name": "ids", - "in": "path", - "description": "A comma-delimited list of stack identifiers.", - "required": true, + "name": "name", + "in": "query", + "description": "The project name to check.", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } } ], "responses": { - "202": { - "description": "Accepted", + "201": { + "description": "The project name is available.", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkInProgressResult" - } - } + "application/json": { } } }, - "400": { - "description": "One or more validation errors occurred." - }, - "404": { - "description": "One or more stacks were not found." - }, - "500": { - "description": "An error occurred while deleting one or more stacks." + "204": { + "description": "The project name is not available." } } } }, - "/api/v2/stacks": { + "/api/v2/organizations/{organizationId}/projects/check-name": { "get": { "tags": [ - "Stack" + "Project" ], - "summary": "Get all", + "summary": "Check for unique name", "parameters": [ { - "name": "filter", + "name": "name", "in": "query", - "description": "A filter that controls what data is returned from the server.", + "description": "The project name to check.", "schema": { "type": "string" } }, { - "name": "sort", - "in": "query", - "description": "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.", + "name": "organizationId", + "in": "path", + "description": "If set the check name will be scoped to a specific organization.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } + } + ], + "responses": { + "201": { + "description": "The project name is available.", + "content": { + "application/json": { } + } }, + "204": { + "description": "The project name is not available." + } + } + } + }, + "/api/v2/projects/{id}/data": { + "post": { + "tags": [ + "Project" + ], + "summary": "Add custom data", + "parameters": [ { - "name": "time", - "in": "query", - "description": "The time filter that limits the data being returned to a specific date range.", + "name": "id", + "in": "path", + "description": "The identifier of the project.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } }, { - "name": "offset", + "name": "key", "in": "query", - "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "description": "The key name of the data object.", "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Any string value.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + } }, - { - "name": "mode", - "in": "query", - "description": "If no mode is set then the whole stack object will be returned. If the mode is set to summary than a lightweight object will be returned.", - "schema": { - "type": "string" + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { } } }, + "400": { + "description": "Invalid key or value." + }, + "404": { + "description": "The project could not be found." + } + } + }, + "delete": { + "tags": [ + "Project" + ], + "summary": "Remove custom data", + "parameters": [ { - "name": "page", - "in": "query", - "description": "The page parameter is used for pagination. This value must be greater than 0.", + "name": "id", + "in": "path", + "description": "The identifier of the project.", + "required": true, "schema": { - "type": "integer", - "format": "int32", - "default": 1 + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" } }, { - "name": "limit", + "name": "key", "in": "query", - "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", + "description": "The key name of the data object.", "schema": { - "type": "integer", - "format": "int32", - "default": 10 + "type": "string" } } ], @@ -4920,98 +5977,43 @@ "200": { "description": "OK", "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Stack" - } - } - } + "application/json": { } } }, "400": { - "description": "Invalid filter." + "description": "Invalid key or value." + }, + "404": { + "description": "The project could not be found." } } } }, - "/api/v2/organizations/{organizationId}/stacks": { + "/api/v2/stacks/{id}": { "get": { "tags": [ "Stack" ], - "summary": "Get by organization", + "summary": "Get by id", + "operationId": "GetStackById", "parameters": [ { - "name": "organizationId", + "name": "id", "in": "path", - "description": "The identifier of the organization.", + "description": "The identifier of the stack.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } }, - { - "name": "filter", - "in": "query", - "description": "A filter that controls what data is returned from the server.", - "schema": { - "type": "string" - } - }, - { - "name": "sort", - "in": "query", - "description": "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.", - "schema": { - "type": "string" - } - }, - { - "name": "time", - "in": "query", - "description": "The time filter that limits the data being returned to a specific date range.", - "schema": { - "type": "string" - } - }, { "name": "offset", "in": "query", - "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", - "schema": { - "type": "string" - } - }, - { - "name": "mode", - "in": "query", - "description": "If no mode is set then the whole stack object will be returned. If the mode is set to summary than a lightweight object will be returned.", + "description": "The time offset in minutes that controls what data is returned based on the `time` filter. This is used for time zone support.", "schema": { "type": "string" } - }, - { - "name": "page", - "in": "query", - "description": "The page parameter is used for pagination. This value must be greater than 0.", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "limit", - "in": "query", - "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", - "schema": { - "type": "integer", - "format": "int32", - "default": 10 - } } ], "responses": { @@ -5020,270 +6022,279 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Stack" - } + "$ref": "#/components/schemas/Stack" } } } }, - "400": { - "description": "Invalid filter." - }, "404": { - "description": "The organization could not be found." - }, - "426": { - "description": "Unable to view stack occurrences for the suspended organization." + "description": "The stack could not be found." } } } }, - "/api/v2/projects/{projectId}/stacks": { - "get": { + "/api/v2/stacks/{ids}/mark-fixed": { + "post": { "tags": [ "Stack" ], - "summary": "Get by project", + "summary": "Mark fixed", "parameters": [ { - "name": "projectId", + "name": "ids", "in": "path", - "description": "The identifier of the project.", + "description": "A comma-delimited list of stack identifiers.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } }, { - "name": "filter", + "name": "version", "in": "query", - "description": "A filter that controls what data is returned from the server.", + "description": "A version number that the stack was fixed in.", "schema": { "type": "string" } - }, - { - "name": "sort", - "in": "query", - "description": "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.", - "schema": { - "type": "string" + } + ], + "responses": { + "202": { + "description": "Accepted", + "content": { + "application/json": { } } }, + "404": { + "description": "One or more stacks could not be found." + } + } + } + }, + "/api/v2/stacks/{ids}/mark-snoozed": { + "post": { + "tags": [ + "Stack" + ], + "summary": "Mark the selected stacks as snoozed", + "parameters": [ { - "name": "time", - "in": "query", - "description": "The time filter that limits the data being returned to a specific date range.", + "name": "ids", + "in": "path", + "description": "A comma-delimited list of stack identifiers.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } }, { - "name": "offset", + "name": "snoozeUntilUtc", "in": "query", - "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "description": "A time that the stack should be snoozed until.", "schema": { - "type": "string" + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "content": { + "application/json": { } } }, + "404": { + "description": "One or more stacks could not be found." + } + } + } + }, + "/api/v2/stacks/{id}/add-link": { + "post": { + "tags": [ + "Stack" + ], + "summary": "Add reference link", + "parameters": [ { - "name": "mode", - "in": "query", - "description": "If no mode is set then the whole stack object will be returned. If the mode is set to summary than a lightweight object will be returned.", + "name": "id", + "in": "path", + "description": "The identifier of the stack.", + "required": true, "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } + } + ], + "requestBody": { + "description": "The reference link.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + } }, - { - "name": "page", - "in": "query", - "description": "The page parameter is used for pagination. This value must be greater than 0.", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { } } }, + "400": { + "description": "Invalid reference link." + }, + "404": { + "description": "The stack could not be found." + } + } + } + }, + "/api/v2/stacks/{id}/remove-link": { + "post": { + "tags": [ + "Stack" + ], + "summary": "Remove reference link", + "parameters": [ { - "name": "limit", - "in": "query", - "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", + "name": "id", + "in": "path", + "description": "The identifier of the stack.", + "required": true, "schema": { - "type": "integer", - "format": "int32", - "default": 10 + "pattern": "^[a-zA-Z\\d]{24,36}$", + "type": "string" } } ], + "requestBody": { + "description": "The reference link.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ValueFromBodyOfstring" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "OK", + "204": { + "description": "The reference link was removed.", "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Stack" - } - } - } + "application/json": { } } }, "400": { - "description": "Invalid filter." + "description": "Invalid reference link." }, "404": { - "description": "The organization could not be found." - }, - "426": { - "description": "Unable to view stack occurrences for the suspended organization." + "description": "The stack could not be found." } } } }, - "/api/v2/organizations/{organizationId}/tokens": { - "get": { + "/api/v2/stacks/{ids}/mark-critical": { + "post": { "tags": [ - "Token" + "Stack" ], - "summary": "Get by organization", + "summary": "Mark future occurrences as critical", "parameters": [ { - "name": "organizationId", + "name": "ids", "in": "path", - "description": "The identifier of the organization.", + "description": "A comma-delimited list of stack identifiers.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } - }, - { - "name": "page", - "in": "query", - "description": "The page parameter is used for pagination. This value must be greater than 0.", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "limit", - "in": "query", - "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", - "schema": { - "type": "integer", - "format": "int32", - "default": 10 - } } ], "responses": { "200": { "description": "OK", "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ViewToken" - } - } - } + "application/json": { } } }, "404": { - "description": "The organization could not be found." + "description": "One or more stacks could not be found." } } }, - "post": { + "delete": { "tags": [ - "Token" + "Stack" ], - "summary": "Create for organization", - "description": "This is a helper action that makes it easier to create a token for a specific organization.\nYou may also specify a scope when creating a token. There are three valid scopes: client, user and admin.", + "summary": "Mark future occurrences as not critical", "parameters": [ { - "name": "organizationId", + "name": "ids", "in": "path", - "description": "The identifier of the organization.", + "description": "A comma-delimited list of stack identifiers.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } } ], - "requestBody": { - "description": "The token.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewToken" - } - } - } - }, "responses": { - "201": { - "description": "Created", + "204": { + "description": "The stacks were marked as not critical.", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewToken" - } - } + "application/json": { } } }, - "400": { - "description": "An error occurred while creating the token." - }, - "409": { - "description": "The token already exists." + "404": { + "description": "One or more stacks could not be found." } } } }, - "/api/v2/projects/{projectId}/tokens": { - "get": { + "/api/v2/stacks/{ids}/change-status": { + "post": { "tags": [ - "Token" + "Stack" ], - "summary": "Get by project", + "summary": "Change stack status", "parameters": [ { - "name": "projectId", + "name": "ids", "in": "path", - "description": "The identifier of the project.", + "description": "A comma-delimited list of stack identifiers.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", "type": "string" } }, { - "name": "page", - "in": "query", - "description": "The page parameter is used for pagination. This value must be greater than 0.", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "limit", + "name": "status", "in": "query", - "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", + "description": "The status that the stack should be changed to.", "schema": { - "type": "integer", - "format": "int32", - "default": 10 + "$ref": "#/components/schemas/StackStatus" } } ], @@ -5291,32 +6302,26 @@ "200": { "description": "OK", "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ViewToken" - } - } - } + "application/json": { } } }, "404": { - "description": "The project could not be found." + "description": "One or more stacks could not be found." } } - }, + } + }, + "/api/v2/stacks/{id}/promote": { "post": { "tags": [ - "Token" + "Stack" ], - "summary": "Create for project", - "description": "This is a helper action that makes it easier to create a token for a specific project.\nYou may also specify a scope when creating a token. There are three valid scopes: client, user and admin.", + "summary": "Promote to external service", "parameters": [ { - "name": "projectId", + "name": "id", "in": "path", - "description": "The identifier of the project.", + "description": "The identifier of the stack.", "required": true, "schema": { "pattern": "^[a-zA-Z\\d]{24,36}$", @@ -5324,54 +6329,139 @@ } } ], - "requestBody": { - "description": "The token.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewToken" - } + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { } + } + }, + "404": { + "description": "The stack could not be found." + }, + "426": { + "description": "Promote to External is a premium feature used to promote an error stack to an external system." + }, + "501": { + "description": "No promoted web hooks are configured for this project." + } + } + } + }, + "/api/v2/stacks/{ids}": { + "delete": { + "tags": [ + "Stack" + ], + "summary": "Remove", + "parameters": [ + { + "name": "ids", + "in": "path", + "description": "A comma-delimited list of stack identifiers.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "Created", + "202": { + "description": "Accepted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ViewToken" + "$ref": "#/components/schemas/WorkInProgressResult" } } } }, "400": { - "description": "An error occurred while creating the token." + "description": "One or more validation errors occurred." }, "404": { - "description": "The project could not be found." + "description": "One or more stacks were not found." }, - "409": { - "description": "The token already exists." + "500": { + "description": "An error occurred while deleting one or more stacks." } } } }, - "/api/v2/projects/{projectId}/tokens/default": { + "/api/v2/stacks": { "get": { "tags": [ - "Token" + "Stack" ], - "summary": "Get a projects default token", + "summary": "Get all", "parameters": [ { - "name": "projectId", - "in": "path", - "description": "The identifier of the project.", - "required": true, + "name": "filter", + "in": "query", + "description": "A filter that controls what data is returned from the server.", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "description": "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.", + "schema": { + "type": "string" + } + }, + { + "name": "time", + "in": "query", + "description": "The time filter that limits the data being returned to a specific date range.", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "schema": { + "type": "string" + } + }, + { + "name": "mode", + "in": "query", + "description": "If no mode is set then the whole stack object will be returned. If the mode is set to summary than a lightweight object will be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "The page parameter is used for pagination. This value must be greater than 0.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 10 } } ], @@ -5381,219 +6471,240 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ViewToken" + "type": "array", + "items": { + "$ref": "#/components/schemas/Stack" + } } } } }, - "404": { - "description": "The project could not be found." + "400": { + "description": "Invalid filter." } } } }, - "/api/v2/tokens/{id}": { + "/api/v2/organizations/{organizationId}/stacks": { "get": { "tags": [ - "Token" + "Stack" ], - "summary": "Get by id", - "operationId": "GetTokenById", + "summary": "Get by organization", "parameters": [ { - "name": "id", + "name": "organizationId", "in": "path", - "description": "The identifier of the token.", + "description": "The identifier of the organization.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d-]{24,40}$", + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewToken" - } - } + }, + { + "name": "filter", + "in": "query", + "description": "A filter that controls what data is returned from the server.", + "schema": { + "type": "string" } }, - "404": { - "description": "The token could not be found." - } - } - }, - "patch": { - "tags": [ - "Token" - ], - "summary": "Update", - "parameters": [ { - "name": "id", - "in": "path", - "description": "The identifier of the token.", - "required": true, + "name": "sort", + "in": "query", + "description": "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.", "schema": { - "pattern": "^[a-zA-Z\\d-]{24,40}(,[a-zA-Z\\d-]{24,40})*$", "type": "string" } - } - ], - "requestBody": { - "description": "The changes", - "content": { - "application/json": { - "schema": { - "description": "A class the tracks changes (i.e. the Delta) for a particular ." - } + }, + { + "name": "time", + "in": "query", + "description": "The time filter that limits the data being returned to a specific date range.", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "schema": { + "type": "string" + } + }, + { + "name": "mode", + "in": "query", + "description": "If no mode is set then the whole stack object will be returned. If the mode is set to summary than a lightweight object will be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "The page parameter is used for pagination. This value must be greater than 0.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 10 } } - }, + ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ViewToken" + "type": "array", + "items": { + "$ref": "#/components/schemas/Stack" + } } } } }, "400": { - "description": "An error occurred while updating the token." + "description": "Invalid filter." }, "404": { - "description": "The token could not be found." + "description": "The organization could not be found." + }, + "426": { + "description": "Unable to view stack occurrences for the suspended organization." } } - }, - "put": { + } + }, + "/api/v2/projects/{projectId}/stacks": { + "get": { "tags": [ - "Token" + "Stack" ], - "summary": "Update", + "summary": "Get by project", "parameters": [ { - "name": "id", + "name": "projectId", "in": "path", - "description": "The identifier of the token.", + "description": "The identifier of the project.", "required": true, "schema": { - "pattern": "^[a-zA-Z\\d-]{24,40}(,[a-zA-Z\\d-]{24,40})*$", + "pattern": "^[a-zA-Z\\d]{24,36}$", "type": "string" } - } - ], - "requestBody": { - "description": "The changes", - "content": { - "application/json": { - "schema": { - "description": "A class the tracks changes (i.e. the Delta) for a particular ." - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewToken" - } - } + }, + { + "name": "filter", + "in": "query", + "description": "A filter that controls what data is returned from the server.", + "schema": { + "type": "string" } }, - "400": { - "description": "An error occurred while updating the token." + { + "name": "sort", + "in": "query", + "description": "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.", + "schema": { + "type": "string" + } }, - "404": { - "description": "The token could not be found." - } - } - } - }, - "/api/v2/tokens": { - "post": { - "tags": [ - "Token" - ], - "summary": "Create", - "description": "To create a new token, you must specify an organization_id. There are three valid scopes: client, user and admin.", - "requestBody": { - "description": "The token.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewToken" - } + { + "name": "time", + "in": "query", + "description": "The time filter that limits the data being returned to a specific date range.", + "schema": { + "type": "string" } - } - }, - "responses": { - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewToken" - } - } + }, + { + "name": "offset", + "in": "query", + "description": "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.", + "schema": { + "type": "string" } }, - "400": { - "description": "An error occurred while creating the token." + { + "name": "mode", + "in": "query", + "description": "If no mode is set then the whole stack object will be returned. If the mode is set to summary than a lightweight object will be returned.", + "schema": { + "type": "string" + } }, - "409": { - "description": "The token already exists." - } - } - } - }, - "/api/v2/tokens/{ids}": { - "delete": { - "tags": [ - "Token" - ], - "summary": "Remove", - "parameters": [ { - "name": "ids", - "in": "path", - "description": "A comma-delimited list of token identifiers.", - "required": true, + "name": "page", + "in": "query", + "description": "The page parameter is used for pagination. This value must be greater than 0.", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "pattern": "^[a-zA-Z\\d-]{24,40}(,[a-zA-Z\\d-]{24,40})*$", - "type": "string" + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 10 } } ], "responses": { - "202": { - "description": "Accepted", + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkInProgressResult" + "type": "array", + "items": { + "$ref": "#/components/schemas/Stack" + } } } } }, "400": { - "description": "One or more validation errors occurred." + "description": "Invalid filter." }, "404": { - "description": "One or more tokens were not found." + "description": "The organization could not be found." }, - "500": { - "description": "An error occurred while deleting one or more tokens." + "426": { + "description": "Unable to view stack occurrences for the suspended organization." } } } @@ -5699,10 +6810,16 @@ "content": { "application/json": { "schema": { - "description": "A class the tracks changes (i.e. the Delta) for a particular ." + "$ref": "#/components/schemas/UpdateUser" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateUser" } } - } + }, + "required": true }, "responses": { "200": { @@ -5745,10 +6862,16 @@ "content": { "application/json": { "schema": { - "description": "A class the tracks changes (i.e. the Delta) for a particular ." + "$ref": "#/components/schemas/UpdateUser" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateUser" } } - } + }, + "required": true }, "responses": { "200": { @@ -5792,7 +6915,11 @@ "in": "query", "description": "The page parameter is used for pagination. This value must be greater than 0.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 1 } @@ -5802,7 +6929,11 @@ "in": "query", "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", "schema": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32", "default": 10 } @@ -5940,7 +7071,10 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { } + } }, "404": { "description": "The user could not be found." @@ -5971,181 +7105,13 @@ ], "responses": { "200": { - "description": "The user verification email has been sent." - }, - "404": { - "description": "The user could not be found." - } - } - } - }, - "/api/v2/projects/{projectId}/webhooks": { - "get": { - "tags": [ - "WebHook" - ], - "summary": "Get by project", - "parameters": [ - { - "name": "projectId", - "in": "path", - "description": "The identifier of the project.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - }, - { - "name": "page", - "in": "query", - "description": "The page parameter is used for pagination. This value must be greater than 0.", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "limit", - "in": "query", - "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.", - "schema": { - "type": "integer", - "format": "int32", - "default": 10 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebHook" - } - } - } - } - }, - "404": { - "description": "The project could not be found." - } - } - } - }, - "/api/v2/webhooks/{id}": { - "get": { - "tags": [ - "WebHook" - ], - "summary": "Get by id", - "operationId": "GetWebHookById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The identifier of the web hook.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}$", - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WebHook" - } - } - } - }, - "404": { - "description": "The web hook could not be found." - } - } - } - }, - "/api/v2/webhooks": { - "post": { - "tags": [ - "WebHook" - ], - "summary": "Create", - "requestBody": { - "description": "The web hook.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewWebHook" - } - } - } - }, - "responses": { - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WebHook" - } - } - } - }, - "400": { - "description": "An error occurred while creating the web hook." - }, - "409": { - "description": "The web hook already exists." - } - } - } - }, - "/api/v2/webhooks/{ids}": { - "delete": { - "tags": [ - "WebHook" - ], - "summary": "Remove", - "parameters": [ - { - "name": "ids", - "in": "path", - "description": "A comma-delimited list of web hook identifiers.", - "required": true, - "schema": { - "pattern": "^[a-zA-Z\\d]{24,36}(,[a-zA-Z\\d]{24,36})*$", - "type": "string" - } - } - ], - "responses": { - "202": { - "description": "Accepted", + "description": "The user verification email has been sent.", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkInProgressResult" - } - } + "application/json": { } } }, - "400": { - "description": "One or more validation errors occurred." - }, "404": { - "description": "One or more web hooks were not found." - }, - "500": { - "description": "An error occurred while deleting one or more web hooks." + "description": "The user could not be found." } } } @@ -6155,9 +7121,9 @@ "schemas": { "BillingPlan": { "required": [ - "description", "id", - "name" + "name", + "description" ], "type": "object", "properties": { @@ -6171,23 +7137,43 @@ "type": "string" }, "price": { - "type": "number", + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], "format": "double" }, "max_projects": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "max_users": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "retention_days": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "max_events_per_month": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "has_premium_features": { @@ -6196,26 +7182,10 @@ "is_hidden": { "type": "boolean" } - }, - "additionalProperties": false + } }, "BillingStatus": { - "enum": [ - 0, - 1, - 2, - 3, - 4 - ], - "type": "integer", - "format": "int32", - "x-enumNames": [ - "Trialing", - "Active", - "PastDue", - "Canceled", - "Unpaid" - ] + "type": "integer" }, "ChangePasswordModel": { "required": [ @@ -6234,8 +7204,7 @@ "minLength": 6, "type": "string" } - }, - "additionalProperties": false + } }, "ChangePlanResult": { "type": "object", @@ -6244,52 +7213,57 @@ "type": "boolean" }, "message": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] } - }, - "additionalProperties": false + } }, "ClientConfiguration": { "type": "object", "properties": { "version": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "settings": { "type": "object", "additionalProperties": { "type": "string" - }, - "readOnly": true + } } - }, - "additionalProperties": false + } }, "CountResult": { "type": "object", "properties": { "total": { - "type": "integer", - "format": "int64" + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int64", + "default": 0 }, "aggregations": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/IAggregate" - }, - "nullable": true + "type": [ + "null", + "object" + ] }, "data": { - "type": "object", - "additionalProperties": { - "nullable": true - }, - "nullable": true + "type": [ + "null", + "object" + ] } - }, - "additionalProperties": false + } }, "ExternalAuthInfo": { "required": [ @@ -6312,30 +7286,18 @@ "type": "string" }, "inviteToken": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "IAggregate": { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "nullable": true - }, - "nullable": true + "type": [ + "null", + "string" + ] } - }, - "additionalProperties": false + } }, "Invite": { "required": [ - "date_added", + "token", "email_address", - "token" + "date_added" ], "type": "object", "properties": { @@ -6349,15 +7311,14 @@ "type": "string", "format": "date-time" } - }, - "additionalProperties": false + } }, "Invoice": { "required": [ - "date", "id", "organization_id", "organization_name", + "date", "paid", "total" ], @@ -6380,7 +7341,11 @@ "type": "boolean" }, "total": { - "type": "number", + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], "format": "double" }, "items": { @@ -6389,13 +7354,12 @@ "$ref": "#/components/schemas/InvoiceLineItem" } } - }, - "additionalProperties": false + } }, "InvoiceGridModel": { "required": [ - "date", "id", + "date", "paid" ], "type": "object", @@ -6410,13 +7374,12 @@ "paid": { "type": "boolean" } - }, - "additionalProperties": false + } }, "InvoiceLineItem": { "required": [ - "amount", - "description" + "description", + "amount" ], "type": "object", "properties": { @@ -6424,15 +7387,41 @@ "type": "string" }, "date": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "amount": { - "type": "number", + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], "format": "double" } - }, - "additionalProperties": false + } + }, + "KeyValuePairOfstringAndStringValues": { + "required": [ + "key", + "value" + ], + "type": "object", + "properties": { + "key": { + "type": [ + "null", + "string" + ] + }, + "value": { + "type": "array", + "items": { + "type": "string" + } + } + } }, "Login": { "required": [ @@ -6442,7 +7431,6 @@ "type": "object", "properties": { "email": { - "minLength": 1, "type": "string", "description": "The email address or domain username" }, @@ -6454,11 +7442,12 @@ "invite_token": { "maxLength": 40, "minLength": 40, - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] } - }, - "additionalProperties": false + } }, "NewOrganization": { "type": "object", @@ -6467,7 +7456,7 @@ "type": "string" } }, - "additionalProperties": false + "description": "A class the tracks changes (i.e. the Delta) for a particular TEntityType." }, "NewProject": { "type": "object", @@ -6481,8 +7470,7 @@ "delete_bot_data_enabled": { "type": "boolean" } - }, - "additionalProperties": false + } }, "NewToken": { "type": "object", @@ -6494,8 +7482,10 @@ "type": "string" }, "default_project_id": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "scopes": { "uniqueItems": true, @@ -6505,16 +7495,19 @@ } }, "expires_utc": { - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "null", + "string" + ], + "format": "date-time" }, "notes": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] } - }, - "additionalProperties": false + } }, "NewWebHook": { "type": "object", @@ -6535,12 +7528,14 @@ } }, "version": { - "type": "string", - "description": "The schema version that should be used.", - "nullable": true + "pattern": "^\\d+(\\.\\d+){1,3}$", + "type": [ + "null", + "string" + ], + "description": "The schema version that should be used." } - }, - "additionalProperties": false + } }, "NotificationSettings": { "type": "object", @@ -6563,8 +7558,7 @@ "report_critical_events": { "type": "boolean" } - }, - "additionalProperties": false + } }, "OAuthAccount": { "required": [ @@ -6584,95 +7578,133 @@ "type": "string" }, "extra_data": { - "type": "object", + "type": [ + "null", + "object" + ], "additionalProperties": { "type": "string" }, "readOnly": true } - }, - "additionalProperties": false + } }, "PersistentEvent": { "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Unique id that identifies an event." }, "organization_id": { - "type": "string" + "type": "string", + "description": "The organization that the event belongs to." }, "project_id": { - "type": "string" + "type": "string", + "description": "The project that the event belongs to." }, "stack_id": { - "type": "string" + "type": "string", + "description": "The stack that the event belongs to." }, "is_first_occurrence": { - "type": "boolean" + "type": "boolean", + "description": "Whether the event resulted in the creation of a new stack." }, "created_utc": { "type": "string", + "description": "The date that the event was created in the system.", "format": "date-time" }, "idx": { "type": "object", - "additionalProperties": { } + "description": "Used to store primitive data type custom data values for searching the event." }, "type": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ], + "description": "The event type (ie. error, log message, feature usage). Check KnownTypes for standard event types." }, "source": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ], + "description": "The event source (ie. machine name, log name, feature name)." }, "date": { "type": "string", + "description": "The date that the event occurred on.", "format": "date-time" }, "tags": { "uniqueItems": true, - "type": "array", + "type": [ + "null", + "array" + ], "items": { "type": "string" }, - "nullable": true + "description": "A list of tags used to categorize this event." }, "message": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ], + "description": "The event message." }, "geo": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ], + "description": "The geo coordinates where the event happened." }, "value": { - "type": "number", - "format": "double", - "nullable": true + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "description": "The value of the event if any.", + "format": "double" }, "count": { - "type": "integer", - "format": "int32", - "nullable": true + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "description": "The number of duplicated events.", + "format": "int32" }, "data": { - "type": "object", - "additionalProperties": { }, - "nullable": true + "type": [ + "null", + "object" + ], + "description": "Optional data entries that contain additional information about this event." }, "reference_id": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ], + "description": "An optional identifier to be used for referencing this event instance at a later time." } - }, - "additionalProperties": false + } }, "ResetPasswordModel": { "required": [ - "password", - "password_reset_token" + "password_reset_token", + "password" ], "type": "object", "properties": { @@ -6686,23 +7718,20 @@ "minLength": 6, "type": "string" } - }, - "additionalProperties": false + } }, "Signup": { "required": [ - "email", "name", + "email", "password" ], "type": "object", "properties": { "name": { - "minLength": 1, "type": "string" }, "email": { - "minLength": 1, "type": "string", "description": "The email address or domain username" }, @@ -6714,90 +7743,122 @@ "invite_token": { "maxLength": 40, "minLength": 40, - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] } - }, - "additionalProperties": false + } }, "Stack": { "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Unique id that identifies a stack." }, "organization_id": { - "type": "string" + "type": "string", + "description": "The organization that the stack belongs to." }, "project_id": { - "type": "string" + "type": "string", + "description": "The project that the stack belongs to." }, "type": { - "type": "string" + "type": "string", + "description": "The stack type (ie. error, log message, feature usage). Check KnownTypes for standard stack types." }, "status": { + "description": "The stack status (ie. open, fixed, regressed,", "$ref": "#/components/schemas/StackStatus" }, "snooze_until_utc": { - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "null", + "string" + ], + "description": "The date that the stack should be snoozed until.", + "format": "date-time" }, "signature_hash": { - "type": "string" + "type": "string", + "description": "The signature used for stacking future occurrences." }, "signature_info": { "type": "object", "additionalProperties": { "type": "string" - } + }, + "description": "The collection of information that went into creating the signature hash for the stack." }, "fixed_in_version": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ], + "description": "The version the stack was fixed in." }, "date_fixed": { - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "null", + "string" + ], + "description": "The date the stack was fixed.", + "format": "date-time" }, "title": { - "type": "string" + "type": "string", + "description": "The stack title." }, "total_occurrences": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "description": "The total number of occurrences in the stack.", "format": "int32" }, "first_occurrence": { "type": "string", + "description": "The date of the 1st occurrence of this stack in UTC time.", "format": "date-time" }, "last_occurrence": { "type": "string", + "description": "The date of the last occurrence of this stack in UTC time.", "format": "date-time" }, "description": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ], + "description": "The stack description." }, "occurrences_are_critical": { - "type": "boolean" + "type": "boolean", + "description": "If true, all future occurrences will be marked as critical." }, "references": { "type": "array", "items": { "type": "string" - } + }, + "description": "A list of references." }, "tags": { "uniqueItems": true, "type": "array", "items": { "type": "string" - } + }, + "description": "A list of tags used to categorize this stack." }, "duplicate_signature": { - "type": "string" + "type": "string", + "description": "The signature used for finding duplicate stacks. (ProjectId, SignatureHash)" }, "created_utc": { "type": "string", @@ -6814,8 +7875,7 @@ "type": "boolean", "readOnly": true } - }, - "additionalProperties": false + } }, "StackStatus": { "enum": [ @@ -6826,7 +7886,6 @@ "ignored", "discarded" ], - "type": "string", "x-enumNames": [ "Open", "Fixed", @@ -6836,56 +7895,75 @@ "Discarded" ] }, - "StringStringValuesKeyValuePair": { + "TokenResult": { + "required": [ + "token" + ], + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "UpdateEmailAddressResult": { + "required": [ + "is_verified" + ], "type": "object", "properties": { - "key": { - "type": "string", - "nullable": true + "is_verified": { + "type": "boolean" + } + } + }, + "UpdateEvent": { + "type": "object", + "properties": { + "email_address": { + "type": "string" }, - "value": { - "type": "array", - "items": { - "type": "string" - } + "description": { + "type": "string" } }, - "additionalProperties": false + "description": "A class the tracks changes (i.e. the Delta) for a particular TEntityType." }, - "StringValueFromBody": { + "UpdateProject": { "type": "object", "properties": { - "value": { - "type": "string", - "nullable": true + "name": { + "type": "string" + }, + "delete_bot_data_enabled": { + "type": "boolean" } }, - "additionalProperties": false + "description": "A class the tracks changes (i.e. the Delta) for a particular TEntityType." }, - "TokenResult": { - "required": [ - "token" - ], + "UpdateToken": { "type": "object", "properties": { - "token": { - "minLength": 1, + "is_disabled": { + "type": "boolean" + }, + "notes": { "type": "string" } }, - "additionalProperties": false + "description": "A class the tracks changes (i.e. the Delta) for a particular TEntityType." }, - "UpdateEmailAddressResult": { - "required": [ - "is_verified" - ], + "UpdateUser": { "type": "object", "properties": { - "is_verified": { + "full_name": { + "type": "string" + }, + "email_notifications_enabled": { "type": "boolean" } }, - "additionalProperties": false + "description": "A class the tracks changes (i.e. the Delta) for a particular TEntityType." }, "UsageHourInfo": { "type": "object", @@ -6895,23 +7973,38 @@ "format": "date-time" }, "total": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "blocked": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "discarded": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "too_big": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } - }, - "additionalProperties": false + } }, "UsageInfo": { "type": "object", @@ -6921,77 +8014,108 @@ "format": "date-time" }, "limit": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "total": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "blocked": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "discarded": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "too_big": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" } - }, - "additionalProperties": false + } }, "User": { "required": [ - "email_address", - "full_name" + "full_name", + "email_address" ], "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Unique id that identifies an user." }, "organization_ids": { "uniqueItems": true, - "type": "array", + "type": [ + "null", + "array" + ], "items": { "type": "string" }, + "description": "The organizations that the user has access to.", "readOnly": true }, "password": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "salt": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "password_reset_token": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "password_reset_token_expiration": { "type": "string", "format": "date-time" }, "o_auth_accounts": { - "type": "array", + "type": [ + "null", + "array" + ], "items": { "$ref": "#/components/schemas/OAuthAccount" }, "readOnly": true }, "full_name": { - "minLength": 1, - "type": "string" + "type": "string", + "description": "Gets or sets the users Full Name." }, "email_address": { - "minLength": 1, - "type": "string", - "format": "email" + "type": "string" }, "email_notifications_enabled": { "type": "boolean" @@ -7000,15 +8124,18 @@ "type": "boolean" }, "verify_email_address_token": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "verify_email_address_token_expiration": { "type": "string", "format": "date-time" }, "is_active": { - "type": "boolean" + "type": "boolean", + "description": "Gets or sets the users active state." }, "roles": { "uniqueItems": true, @@ -7025,34 +8152,54 @@ "type": "string", "format": "date-time" } - }, - "additionalProperties": false + } }, "UserDescription": { "type": "object", "properties": { "email_address": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "description": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "data": { - "type": "object", - "additionalProperties": { }, - "nullable": true + "type": [ + "null", + "object" + ], + "description": "Extended data entries for this user description." } - }, - "additionalProperties": false + } + }, + "ValueFromBodyOfstring": { + "required": [ + "value" + ], + "type": "object", + "properties": { + "value": { + "type": [ + "null", + "string" + ] + } + } }, "ViewCurrentUser": { "type": "object", "properties": { "hash": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "has_local_account": { "type": "boolean" @@ -7098,8 +8245,7 @@ "type": "string" } } - }, - "additionalProperties": false + } }, "ViewOrganization": { "type": "object", @@ -7124,84 +8270,136 @@ "type": "string" }, "card_last4": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "subscribe_date": { - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "null", + "string" + ], + "format": "date-time" }, "billing_change_date": { - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "null", + "string" + ], + "format": "date-time" }, "billing_changed_by_user_id": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "billing_status": { "$ref": "#/components/schemas/BillingStatus" }, "billing_price": { - "type": "number", + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], "format": "double" }, "max_events_per_month": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "bonus_events_per_month": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "bonus_expiration": { - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "null", + "string" + ], + "format": "date-time" }, "retention_days": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "is_suspended": { "type": "boolean" }, "suspension_code": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "suspension_notes": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "suspension_date": { - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "null", + "string" + ], + "format": "date-time" }, "has_premium_features": { "type": "boolean" }, "max_users": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "max_projects": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "project_count": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int64" }, "stack_count": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int64" }, "event_count": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int64" }, "invites": { @@ -7223,9 +8421,10 @@ } }, "data": { - "type": "object", - "additionalProperties": { }, - "nullable": true + "type": [ + "null", + "object" + ] }, "is_throttled": { "type": "boolean" @@ -7236,8 +8435,7 @@ "is_over_request_limit": { "type": "boolean" } - }, - "additionalProperties": false + } }, "ViewProject": { "type": "object", @@ -7262,9 +8460,10 @@ "type": "boolean" }, "data": { - "type": "object", - "additionalProperties": { }, - "nullable": true + "type": [ + "null", + "object" + ] }, "promoted_tabs": { "uniqueItems": true, @@ -7274,15 +8473,25 @@ } }, "is_configured": { - "type": "boolean", - "nullable": true + "type": [ + "null", + "boolean" + ] }, "stack_count": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int64" }, "event_count": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int64" }, "has_premium_features": { @@ -7303,8 +8512,7 @@ "$ref": "#/components/schemas/UsageInfo" } } - }, - "additionalProperties": false + } }, "ViewToken": { "type": "object", @@ -7319,12 +8527,16 @@ "type": "string" }, "user_id": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "default_project_id": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "scopes": { "uniqueItems": true, @@ -7334,13 +8546,17 @@ } }, "expires_utc": { - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "null", + "string" + ], + "format": "date-time" }, "notes": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "is_disabled": { "type": "boolean" @@ -7356,8 +8572,7 @@ "type": "string", "format": "date-time" } - }, - "additionalProperties": false + } }, "ViewUser": { "type": "object", @@ -7397,8 +8612,7 @@ "type": "string" } } - }, - "additionalProperties": false + } }, "WebHook": { "type": "object", @@ -7425,26 +8639,29 @@ "type": "boolean" }, "version": { - "type": "string" + "type": "string", + "description": "The schema version that should be used." }, "created_utc": { "type": "string", "format": "date-time" } - }, - "additionalProperties": false + } }, "WorkInProgressResult": { "type": "object", "properties": { "workers": { - "type": "array", + "type": [ + "null", + "array" + ], "items": { "type": "string" - } + }, + "readOnly": true } - }, - "additionalProperties": false + } } }, "securitySchemes": { @@ -7474,6 +8691,12 @@ } ], "tags": [ + { + "name": "Token" + }, + { + "name": "WebHook" + }, { "name": "Auth" }, @@ -7489,14 +8712,8 @@ { "name": "Stack" }, - { - "name": "Token" - }, { "name": "User" - }, - { - "name": "WebHook" } ] } \ No newline at end of file diff --git a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs index e62b92e1ab..ca06918289 100644 --- a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs @@ -1082,10 +1082,11 @@ await SendRequestAsync(r => r .StatusCodeShouldBeNotFound() ); + // /docs/{documentName} is now handled by Scalar API documentation await SendRequestAsync(r => r .BaseUri(_server.BaseAddress) .AppendPaths("docs", "blah") - .StatusCodeShouldBeNotFound() + .StatusCodeShouldBeOk() ); } diff --git a/tests/Exceptionless.Tests/Controllers/OpenApiControllerTests.cs b/tests/Exceptionless.Tests/Controllers/OpenApiControllerTests.cs index 7ec16b84e3..0f1d99dc51 100644 --- a/tests/Exceptionless.Tests/Controllers/OpenApiControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/OpenApiControllerTests.cs @@ -14,12 +14,12 @@ public OpenApiControllerTests(ITestOutputHelper output, AppWebHostFactory factor public async Task GetSwaggerJson_Default_ReturnsExpectedBaseline() { // Arrange - string baselinePath = Path.Combine("..", "..", "..", "Controllers", "Data", "swagger.json"); + string baselinePath = Path.Combine("..", "..", "..", "Controllers", "Data", "openapi.json"); // Act var response = await SendRequestAsync(r => r .BaseUri(_server.BaseAddress) - .AppendPaths("docs", "v2", "swagger.json") + .AppendPaths("docs", "v2", "openapi.json") .StatusCodeShouldBeOk() ); diff --git a/tests/Exceptionless.Tests/Serializer/LowerCaseUnderscoreNamingPolicyTests.cs b/tests/Exceptionless.Tests/Serializer/LowerCaseUnderscoreNamingPolicyTests.cs new file mode 100644 index 0000000000..82b688a426 --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/LowerCaseUnderscoreNamingPolicyTests.cs @@ -0,0 +1,181 @@ +using System.Text.Json; +using Exceptionless.Core.Models; +using Exceptionless.Core.Models.Data; +using Exceptionless.Web.Models; +using Exceptionless.Web.Utility; +using Foundatio.Xunit; +using Xunit; +using Xunit.Abstractions; + +namespace Exceptionless.Tests.Serializer; + +/// +/// Tests for LowerCaseUnderscoreNamingPolicy and System.Text.Json serialization for the API layer. +/// +public class LowerCaseUnderscoreNamingPolicyTests : TestWithLoggingBase +{ + public LowerCaseUnderscoreNamingPolicyTests(ITestOutputHelper output) : base(output) { } + + private static readonly JsonSerializerOptions ApiOptions = new() + { + PropertyNamingPolicy = LowerCaseUnderscoreNamingPolicy.Instance, + Converters = { new DeltaJsonConverterFactory() } + }; + + [Fact] + public void NamingPolicy_Instance_ReturnsSingleton() + { + var instance1 = LowerCaseUnderscoreNamingPolicy.Instance; + var instance2 = LowerCaseUnderscoreNamingPolicy.Instance; + + Assert.Same(instance1, instance2); + } + + [Fact] + public void NamingPolicy_AppOptionsProperties_SerializesCorrectly() + { + var model = new AppOptionsModel + { + BaseURL = "https://example.com", + EnableSSL = true, + MaximumRetentionDays = 180, + WebsiteMode = "production" + }; + + string json = JsonSerializer.Serialize(model, ApiOptions); + + Assert.Contains("\"base_u_r_l\":\"https://example.com\"", json); + Assert.Contains("\"enable_s_s_l\":true", json); + Assert.Contains("\"maximum_retention_days\":180", json); + Assert.Contains("\"website_mode\":\"production\"", json); + } + + [Fact] + public void NamingPolicy_EnvironmentProperties_SerializesCorrectly() + { + // Properties from event-serialization-input.json + var model = new EnvironmentModel + { + OSName = "Windows 11", + OSVersion = "10.0.22621", + IPAddress = "192.168.1.100", + MachineName = "TEST-MACHINE" + }; + + string json = JsonSerializer.Serialize(model, ApiOptions); + + Assert.Contains("\"o_s_name\":\"Windows 11\"", json); + Assert.Contains("\"o_s_version\":\"10.0.22621\"", json); + Assert.Contains("\"i_p_address\":\"192.168.1.100\"", json); + Assert.Contains("\"machine_name\":\"TEST-MACHINE\"", json); + } + + [Fact] + public void ExternalAuthInfo_Serialize_UsesCamelCasePropertyNames() + { + var authInfo = new ExternalAuthInfo + { + ClientId = "test-client", + Code = "auth-code", + RedirectUri = "https://example.com/callback", + InviteToken = "token123" + }; + + string json = JsonSerializer.Serialize(authInfo, ApiOptions); + + // ExternalAuthInfo uses explicit JsonPropertyName attributes (camelCase) + Assert.Contains("\"clientId\":\"test-client\"", json); + Assert.Contains("\"code\":\"auth-code\"", json); + Assert.Contains("\"redirectUri\":\"https://example.com/callback\"", json); + Assert.Contains("\"inviteToken\":\"token123\"", json); + } + + [Fact] + public void ExternalAuthInfo_Deserialize_ParsesCamelCaseJson() + { + string json = """{"clientId":"my-client","code":"my-code","redirectUri":"https://test.com"}"""; + + var authInfo = JsonSerializer.Deserialize(json, ApiOptions); + + Assert.NotNull(authInfo); + Assert.Equal("my-client", authInfo.ClientId); + Assert.Equal("my-code", authInfo.Code); + Assert.Equal("https://test.com", authInfo.RedirectUri); + Assert.Null(authInfo.InviteToken); + } + + [Fact] + public void Delta_Deserialize_SnakeCaseJson_SetsPropertyValues() + { + string json = """{"data":"TestValue","is_active":true}"""; + + var delta = JsonSerializer.Deserialize>(json, ApiOptions); + + Assert.NotNull(delta); + Assert.True(delta.TryGetPropertyValue("Data", out object? dataValue)); + Assert.Equal("TestValue", dataValue); + Assert.True(delta.TryGetPropertyValue("IsActive", out object? isActiveValue)); + Assert.Equal(true, isActiveValue); + } + + [Fact] + public void Delta_Deserialize_PartialUpdate_OnlyTracksProvidedProperties() + { + string json = """{"is_active":false}"""; + + var delta = JsonSerializer.Deserialize>(json, ApiOptions); + + Assert.NotNull(delta); + var changedProperties = delta.GetChangedPropertyNames(); + Assert.Single(changedProperties); + Assert.Contains("IsActive", changedProperties); + } + + [Fact] + public void StackStatus_Serialize_UsesStringValue() + { + var stack = new StackStatusModel { Status = StackStatus.Fixed }; + + string json = JsonSerializer.Serialize(stack, ApiOptions); + + Assert.Contains("\"status\":\"fixed\"", json); + } + + [Fact] + public void StackStatus_Deserialize_ParsesStringValue() + { + string json = """{"status":"regressed"}"""; + + var model = JsonSerializer.Deserialize(json, ApiOptions); + + Assert.NotNull(model); + Assert.Equal(StackStatus.Regressed, model.Status); + } + + private class AppOptionsModel + { + public string? BaseURL { get; set; } + public bool EnableSSL { get; set; } + public int MaximumRetentionDays { get; set; } + public string? WebsiteMode { get; set; } + } + + private class EnvironmentModel + { + public string? OSName { get; set; } + public string? OSVersion { get; set; } + public string? IPAddress { get; set; } + public string? MachineName { get; set; } + } + + private class SimpleModel + { + public string? Data { get; set; } + public bool IsActive { get; set; } + } + + private class StackStatusModel + { + public StackStatus Status { get; set; } + } +} diff --git a/tests/http/admin.http b/tests/http/admin.http index a1f9aa331c..99e14ef801 100644 --- a/tests/http/admin.http +++ b/tests/http/admin.http @@ -1,4 +1,5 @@ -@apiUrl = http://localhost:5200/api/v2 +@url = http://localhost:5200 +@apiUrl = {url}/api/v2 @email = test@localhost @password = tester @organizationId = 537650f3b77efe23a47914f3 @@ -22,6 +23,10 @@ Content-Type: application/json GET {{apiUrl}}/users/me Authorization: Bearer {{token}} +### Get OpenApi schema +# @name openapi +GET {{url}}/openapi/v1.json + ### @userId = {{currentUser.response.body.$.id}}