Browse Source

Schema generation.

pull/1/head
Sebastian 9 years ago
parent
commit
08d5270af5
  1. 43
      src/Squidex.Core/Contents/ContentData.cs
  2. 49
      src/Squidex.Core/Contents/ContentFieldData.cs
  3. 1
      src/Squidex.Core/Schemas/BooleanField.cs
  4. 56
      src/Squidex.Core/Schemas/Field.cs
  5. 21
      src/Squidex.Core/Schemas/Json/JsonFieldModel.cs
  6. 23
      src/Squidex.Core/Schemas/Json/JsonSchemaModel.cs
  7. 30
      src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs
  8. 120
      src/Squidex.Core/Schemas/Schema.cs
  9. 2
      src/Squidex.Core/project.json
  10. 4
      src/Squidex.Events/Contents/ContentCreated.cs
  11. 4
      src/Squidex.Events/Contents/ContentUpdated.cs
  12. 62
      src/Squidex.Infrastructure.RabbitMq/RabbitMqEventChannel.cs
  13. 2
      src/Squidex.Infrastructure/CQRS/Events/IEventStream.cs
  14. 2
      src/Squidex.Infrastructure/DomainObjectNotFoundException.cs
  15. 2
      src/Squidex.Infrastructure/Language.cs
  16. 2
      src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs
  17. 3
      src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs
  18. 9
      src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs
  19. 4
      src/Squidex.Write/Contents/ContentDataCommand.cs
  20. 53
      src/Squidex/Config/Identity/SwaggerIdentityUsage.cs
  21. 85
      src/Squidex/Config/Swagger/SwaggerServices.cs
  22. 47
      src/Squidex/Config/Swagger/SwaggerUsage.cs
  23. 2
      src/Squidex/Controllers/Api/Apps/AppClientsController.cs
  24. 4
      src/Squidex/Controllers/Api/Docs/DocsController.cs
  25. 2
      src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs
  26. 2
      src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs
  27. 64
      src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs
  28. 356
      src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs
  29. 56
      src/Squidex/Controllers/ContentApi/Models/ContentEntryDto.cs
  30. 89
      src/Squidex/Pipeline/Swagger/SwaggerHelper.cs
  31. 10
      src/Squidex/Startup.cs
  32. 2
      src/Squidex/Views/Shared/Docs.cshtml
  33. 15
      tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs
  34. 4
      tests/Squidex.Core.Tests/Schemas/SchemaTests.cs
  35. 64
      tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs
  36. 11
      tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs
  37. 4
      tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs

43
src/Squidex.Core/Contents/ContentData.cs

@ -0,0 +1,43 @@
// ==========================================================================
// ContentData.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Immutable;
using Squidex.Infrastructure;
namespace Squidex.Core.Contents
{
public sealed class ContentData
{
private readonly ImmutableDictionary<string, ContentFieldData> fields;
public ImmutableDictionary<string, ContentFieldData> Fields
{
get { return fields; }
}
public ContentData(ImmutableDictionary<string, ContentFieldData> fields)
{
Guard.NotNull(fields, nameof(fields));
this.fields = fields;
}
public static ContentData Empty()
{
return new ContentData(ImmutableDictionary<string, ContentFieldData>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase));
}
public ContentData AddField(string fieldName, ContentFieldData data)
{
Guard.ValidPropertyName(fieldName, nameof(fieldName));
return new ContentData(Fields.Add(fieldName, data));
}
}
}

49
src/Squidex.Core/Contents/ContentFieldData.cs

@ -0,0 +1,49 @@
// ==========================================================================
// ContentFieldData.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Immutable;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure;
namespace Squidex.Core.Contents
{
public sealed class ContentFieldData
{
private readonly ImmutableDictionary<string, JToken> valueByLanguage;
public ImmutableDictionary<string, JToken> ValueByLanguage
{
get { return valueByLanguage; }
}
public ContentFieldData(ImmutableDictionary<string, JToken> valueByLanguage)
{
Guard.NotNull(valueByLanguage, nameof(valueByLanguage));
this.valueByLanguage = valueByLanguage;
}
public static ContentFieldData New()
{
return new ContentFieldData(ImmutableDictionary<string, JToken>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase));
}
public ContentFieldData AddValue(JToken value)
{
return new ContentFieldData(valueByLanguage.Add("iv", value));
}
public ContentFieldData AddValue(string language, JToken value)
{
Guard.NotNullOrEmpty(language, nameof(language));
return new ContentFieldData(valueByLanguage.Add(language, value));
}
}
}

1
src/Squidex.Core/Schemas/BooleanField.cs

@ -6,7 +6,6 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using NJsonSchema;

56
src/Squidex.Core/Schemas/Field.cs

@ -14,6 +14,7 @@ using NJsonSchema;
using Squidex.Infrastructure;
// ReSharper disable InvertIf
// ReSharper disable ConvertIfStatementToReturnStatement
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
namespace Squidex.Core.Schemas
{
@ -128,56 +129,51 @@ namespace Squidex.Core.Schemas
return Clone<Field>(clone => clone.name = newName);
}
public void AddToSchema(JsonSchema4 schema, HashSet<Language> languages)
public void AddToSchema(JsonSchema4 schema, IEnumerable<Language> languages, string schemaName, Func<string, JsonSchema4, JsonSchema4> schemaResolver)
{
Guard.NotNull(schema, nameof(schema));
Guard.NotEmpty(languages, nameof(languages));
Guard.NotNull(languages, nameof(languages));
Guard.NotNull(schemaResolver, nameof(schemaResolver));
if (RawProperties.IsLocalizable)
if (!RawProperties.IsLocalizable)
{
var localizableProperty = new JsonProperty { IsRequired = true, Type = JsonObjectType.Object };
var localizableType = new JsonSchema4 { Id = $"{Name}ByLanguage" };
foreach (var language in languages)
{
var languageProperty = CreateProperty();
languages = new[] { Language.Invariant };
}
if (!string.IsNullOrWhiteSpace(languageProperty.Title))
{
languageProperty.Title += $" ({language.EnglishName})";
}
var languagesProperty = CreateProperty();
var languagesObject = new JsonSchema4 { Type = JsonObjectType.Object, AllowAdditionalProperties = false };
localizableType.Properties.Add(language.Iso2Code, languageProperty);
}
foreach (var language in languages)
{
var languageProperty = new JsonProperty { Description = language.EnglishName };
localizableProperty.OneOf.Add(localizableType);
PrepareJsonSchema(languageProperty);
schema.Properties.Add(Name, localizableProperty);
}
else
{
schema.Properties.Add(Name, CreateProperty());
languagesObject.Properties.Add(language.Iso2Code, languageProperty);
}
languagesProperty.AllOf.Add(schemaResolver($"{schemaName}{Name}Property", languagesObject));
schema.Properties.Add(Name, languagesProperty);
}
public JsonProperty CreateProperty()
{
var jsonProperty = new JsonProperty { IsRequired = RawProperties.IsRequired };
var jsonProperty = new JsonProperty { IsRequired = RawProperties.IsRequired, Type = JsonObjectType.Object };
if (!string.IsNullOrWhiteSpace(RawProperties.Hints))
{
jsonProperty.Title = RawProperties.Hints;
}
else if (!string.IsNullOrWhiteSpace(RawProperties.Label))
if (!string.IsNullOrWhiteSpace(RawProperties.Label))
{
jsonProperty.Title = $"The {RawProperties.Label} field";
jsonProperty.Description = RawProperties.Label;
}
else
{
jsonProperty.Title = $"The {Name} field";
jsonProperty.Description = Name;
}
PrepareJsonSchema(jsonProperty);
if (!string.IsNullOrWhiteSpace(RawProperties.Hints))
{
jsonProperty.Description += $" ({RawProperties.Hints}).";
}
return jsonProperty;
}

21
src/Squidex.Core/Schemas/Json/JsonFieldModel.cs

@ -0,0 +1,21 @@
// ==========================================================================
// JsonFieldModel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Core.Schemas.Json
{
public sealed class JsonFieldModel
{
public string Name;
public bool IsHidden;
public bool IsDisabled;
public FieldProperties Properties;
}
}

23
src/Squidex.Core/Schemas/Json/JsonSchemaModel.cs

@ -0,0 +1,23 @@
// ==========================================================================
// JsonSchemaModel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Core.Schemas.Json
{
public sealed class JsonSchemaModel
{
public string Name;
public bool IsPublished;
public SchemaProperties Properties;
public Dictionary<long, JsonFieldModel> Fields;
}
}

30
src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs

@ -21,28 +21,6 @@ namespace Squidex.Core.Schemas.Json
private readonly FieldRegistry fieldRegistry;
private readonly JsonSerializer serializer;
public class FieldModel
{
public string Name;
public bool IsHidden;
public bool IsDisabled;
public FieldProperties Properties;
}
public sealed class SchemaModel
{
public string Name;
public bool IsPublished;
public SchemaProperties Properties;
public Dictionary<long, FieldModel> Fields;
}
public SchemaJsonSerializer(FieldRegistry fieldRegistry, JsonSerializerSettings serializerSettings)
{
Guard.NotNull(fieldRegistry, nameof(fieldRegistry));
@ -55,13 +33,13 @@ namespace Squidex.Core.Schemas.Json
public JToken Serialize(Schema schema)
{
var model = new SchemaModel { Name = schema.Name, IsPublished = schema.IsPublished, Properties = schema.Properties };
var model = new JsonSchemaModel { Name = schema.Name, IsPublished = schema.IsPublished, Properties = schema.Properties };
model.Fields =
schema.Fields
.Select(x =>
new KeyValuePair<long, FieldModel>(x.Key,
new FieldModel
new KeyValuePair<long, JsonFieldModel>(x.Key,
new JsonFieldModel
{
Name = x.Value.Name,
IsHidden = x.Value.IsHidden,
@ -75,7 +53,7 @@ namespace Squidex.Core.Schemas.Json
public Schema Deserialize(JToken token)
{
var model = token.ToObject<SchemaModel>(serializer);
var model = token.ToObject<JsonSchemaModel>(serializer);
var fields =
model.Fields.Select(kvp =>

120
src/Squidex.Core/Schemas/Schema.cs

@ -13,7 +13,9 @@ using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using NJsonSchema;
using Squidex.Core.Contents;
using Squidex.Infrastructure;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable InvertIf
@ -166,127 +168,79 @@ namespace Squidex.Core.Schemas
return AddOrUpdateField(newField);
}
public JsonSchema4 BuildSchema(HashSet<Language> languages)
public JsonSchema4 BuildSchema(HashSet<Language> languages, Func<string, JsonSchema4, JsonSchema4> schemaResolver)
{
Guard.NotEmpty(languages, nameof(languages));
Guard.NotNull(schemaResolver, nameof(schemaResolver));
var schema = new JsonSchema4 { Id = Name };
if (!string.IsNullOrWhiteSpace(Properties.Hints))
{
schema.Title = Properties.Hints;
}
else if (!string.IsNullOrWhiteSpace(Properties.Label))
{
schema.Title = $"The {Properties.Label} field";
}
else
{
schema.Title = $"The {Name} field";
}
var schema = new JsonSchema4 { Id = Name, Type = JsonObjectType.Object };
foreach (var field in fieldsByName.Values)
{
field.AddToSchema(schema, languages);
field.AddToSchema(schema, languages, Name, schemaResolver);
}
return schema;
}
public async Task ValidateAsync(JObject data, IList<ValidationError> errors, HashSet<Language> languages)
public async Task ValidateAsync(ContentData data, IList<ValidationError> errors, HashSet<Language> languages)
{
Guard.NotNull(data, nameof(data));
Guard.NotNull(errors, nameof(errors));
Guard.NotEmpty(languages, nameof(languages));
AppendEmptyFields(data, languages);
foreach (var fieldValue in data.Fields)
{
if (!fieldsByName.ContainsKey(fieldValue.Key))
{
errors.Add(new ValidationError($"{fieldValue.Key} is not a known field", fieldValue.Key));
}
}
foreach (var property in data.Properties())
foreach (var field in fieldsByName.Values)
{
var fieldErrors = new List<string>();
Field field;
if (fieldsByName.TryGetValue(property.Name, out field))
var fieldData = data.Fields.GetOrDefault(field.Name) ?? ContentFieldData.New();
if (field.RawProperties.IsLocalizable)
{
if (field.RawProperties.IsLocalizable)
foreach (var valueLanguage in fieldData.ValueByLanguage.Keys)
{
var languageObject = property.Value as JObject;
Language language;
if (languageObject == null)
if (!Language.TryGetLanguage(valueLanguage, out language))
{
fieldErrors.Add($"{property.Name} is localizable and must be an object");
fieldErrors.Add($"{field.Name} has an invalid language '{valueLanguage}'");
}
else
else if (!languages.Contains(language))
{
AppendEmptyLanguages(languageObject, languages);
foreach (var languageProperty in languageObject.Properties())
{
Language language;
if (!Language.TryGetLanguage(languageProperty.Name, out language))
{
fieldErrors.Add($"{property.Name} has an invalid language '{languageProperty.Name}'");
continue;
}
if (!languages.Contains(language))
{
fieldErrors.Add($"{property.Name} has an unsupported language '{languageProperty.Name}'");
continue;
}
await field.ValidateAsync(languageProperty.Value, fieldErrors, language);
}
fieldErrors.Add($"{field.Name} has an unsupported language '{valueLanguage}'");
}
}
else
foreach (var language in languages)
{
await field.ValidateAsync(property.Value, fieldErrors);
var value = fieldData.ValueByLanguage.GetValueOrDefault(language.Iso2Code, JValue.CreateNull());
await field.ValidateAsync(value, fieldErrors, language);
}
}
else
{
fieldErrors.Add($"{property.Name} is not a known field");
}
foreach (var error in fieldErrors)
{
errors.Add(new ValidationError(error, property.Name));
}
}
}
if (fieldData.ValueByLanguage.Keys.Any(x => x != "iv"))
{
fieldErrors.Add($"{field.Name} can only contain a single entry for invariant language (iv)");
}
private static void AppendEmptyLanguages(JObject data, IEnumerable<Language> languages)
{
var nullJson = JValue.CreateNull();
var value = fieldData.ValueByLanguage.GetValueOrDefault("iv", JValue.CreateNull());
foreach (var language in languages)
{
if (data.GetValue(language.Iso2Code, StringComparison.OrdinalIgnoreCase) == null)
{
data.Add(new JProperty(language.Iso2Code, nullJson));
await field.ValidateAsync(value, fieldErrors);
}
}
}
private void AppendEmptyFields(JObject data, HashSet<Language> languages)
{
var nullJson = JValue.CreateNull();
foreach (var field in fieldsByName.Values)
{
if (data.GetValue(field.Name, StringComparison.OrdinalIgnoreCase) == null)
foreach (var error in fieldErrors)
{
JToken value = nullJson;
if (field.RawProperties.IsLocalizable)
{
value = new JObject(languages.Select(x => new JProperty(x.Iso2Code, nullJson)).OfType<object>().ToArray());
}
data.Add(new JProperty(field.Name, value));
errors.Add(new ValidationError(error, field.Name));
}
}
}

2
src/Squidex.Core/project.json

@ -5,7 +5,7 @@
"protobuf-net": "2.1.0",
"NETStandard.Library": "1.6.1",
"Microsoft.NETCore.App": "1.1.0",
"NJsonSchema": "7.6.6221.22528"
"NJsonSchema": "7.3.6214.20986"
},
"frameworks": {
"netcoreapp1.0": {

4
src/Squidex.Events/Contents/ContentCreated.cs

@ -7,7 +7,7 @@
// ==========================================================================
using System;
using Newtonsoft.Json.Linq;
using Squidex.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
@ -18,6 +18,6 @@ namespace Squidex.Events.Contents
{
public Guid SchemaId { get; set; }
public JObject Data { get; set; }
public ContentData Data { get; set; }
}
}

4
src/Squidex.Events/Contents/ContentUpdated.cs

@ -6,7 +6,7 @@
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json.Linq;
using Squidex.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
@ -15,6 +15,6 @@ namespace Squidex.Events.Contents
[TypeName("ContentUpdatedEvent")]
public class ContentUpdated : IEvent
{
public JObject Data { get; set; }
public ContentData Data { get; set; }
}
}

62
src/Squidex.Infrastructure.RabbitMq/RabbitMqEventChannel.cs

@ -12,69 +12,83 @@ using Newtonsoft.Json;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using Squidex.Infrastructure.CQRS.Events;
// ReSharper disable InvertIf
namespace Squidex.Infrastructure.RabbitMq
{
public sealed class RabbitMqEventChannel : DisposableObject, IEventPublisher, IEventStream
{
private const string Exchange = "Squidex";
private readonly Lazy<IModel> currentChannel;
private readonly IConnection connection;
private readonly IModel channel;
private EventingBasicConsumer consumer;
public RabbitMqEventChannel(IConnectionFactory connectionFactory)
{
Guard.NotNull(connectionFactory, nameof(connectionFactory));
currentChannel = new Lazy<IModel>(() => Connect(connectionFactory));
connection = connectionFactory.CreateConnection();
channel = CreateChannel(connection);
}
protected override void DisposeObject(bool disposing)
{
if (currentChannel.IsValueCreated)
{
currentChannel.Value.Dispose();
}
connection.Close();
connection.Dispose();
}
public void Publish(EventData eventData)
{
ThrowIfDisposed();
var channel = currentChannel.Value;
channel.BasicPublish(Exchange, string.Empty, null, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(eventData)));
}
public void Connect(string queueName, Action<EventData> received)
{
ThrowIfDisposed();
ThrowIfConnected();
var channel = currentChannel.Value;
queueName = $"{queueName}_{Environment.MachineName}";
lock (connection)
{
ThrowIfConnected();
queueName = $"{queueName}_{Environment.MachineName}";
channel.QueueDeclare(queueName, true, false, false);
channel.QueueBind(queueName, Exchange, string.Empty);
channel.QueueDeclare(queueName, true, false, false);
channel.QueueBind(queueName, Exchange, string.Empty);
var consumer = new EventingBasicConsumer(channel);
consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, e) =>
{
var eventData = JsonConvert.DeserializeObject<EventData>(Encoding.UTF8.GetString(e.Body));
consumer.Received += (model, e) =>
{
var eventData = JsonConvert.DeserializeObject<EventData>(Encoding.UTF8.GetString(e.Body));
received(eventData);
};
received(eventData);
};
channel.BasicConsume(queueName, true, consumer);
channel.BasicConsume(queueName, true, consumer);
}
}
private static IModel Connect(IConnectionFactory connectionFactory)
private static IModel CreateChannel(IConnection connection, bool declareExchange = true)
{
var connection = connectionFactory.CreateConnection();
var channel = connection.CreateModel();
channel.ExchangeDeclare(Exchange, ExchangeType.Fanout, true);
if (declareExchange)
{
channel.ExchangeDeclare(Exchange, ExchangeType.Fanout, true);
}
return channel;
}
private void ThrowIfConnected()
{
if (consumer != null)
{
throw new InvalidOperationException("Already connected to channel.");
}
}
}
}

2
src/Squidex.Infrastructure/CQRS/Events/IEventStream.cs

@ -10,7 +10,7 @@ using System;
namespace Squidex.Infrastructure.CQRS.Events
{
public interface IEventStream
public interface IEventStream : IDisposable
{
void Connect(string queueName, Action<EventData> received);
}

2
src/Squidex.Infrastructure/DomainObjectNotFoundException.cs

@ -24,7 +24,7 @@ namespace Squidex.Infrastructure
private static string FormatMessage(string id, Type type)
{
return $"Domain object \'{id}\' (type {type}) not found.";
return $"Domain object \'{id}\' (type {type}) is not found.";
}
private static string FormatMessage(string id, string collection, Type type)

2
src/Squidex.Infrastructure/Language.cs

@ -20,6 +20,8 @@ namespace Squidex.Infrastructure
private readonly string englishName;
private static readonly Dictionary<string, Language> allLanguages = new Dictionary<string, Language>();
public static readonly Language Invariant = new Language("iv", "Invariant");
static Language()
{
var resourceAssembly = typeof(Language).GetTypeInfo().Assembly;

2
src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs

@ -16,6 +16,8 @@ namespace Squidex.Read.Schemas.Repositories
{
Task<IReadOnlyList<ISchemaEntity>> QueryAllAsync(Guid appId);
Task<IReadOnlyList<ISchemaEntityWithSchema>> QueryAllWithSchemaAsync(Guid appId);
Task<Guid?> FindSchemaIdAsync(Guid appId, string name);
Task<ISchemaEntityWithSchema> FindSchemaAsync(Guid appId, string name);

3
src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs

@ -8,6 +8,7 @@
using System;
using MongoDB.Bson.Serialization.Attributes;
using Newtonsoft.Json.Linq;
using Squidex.Core.Schemas;
using Squidex.Core.Schemas.Json;
using Squidex.Infrastructure;
@ -59,7 +60,7 @@ namespace Squidex.Store.MongoDb.Schemas
public Lazy<Schema> DeserializeSchema(SchemaJsonSerializer serializer)
{
schema = new Lazy<Schema>(() => Schema != null ? serializer.Deserialize(Schema) : null);
schema = new Lazy<Schema>(() => Schema != null ? serializer.Deserialize(JObject.Parse(Schema)) : null);
return schema;
}

9
src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs

@ -65,6 +65,15 @@ namespace Squidex.Store.MongoDb.Schemas
return entities.OfType<ISchemaEntity>().ToList();
}
public async Task<IReadOnlyList<ISchemaEntityWithSchema>> QueryAllWithSchemaAsync(Guid appId)
{
var entities = await Collection.Find(s => s.AppId == appId && !s.IsDeleted).ToListAsync();
entities.ForEach(x => x.DeserializeSchema(serializer));
return entities.OfType<ISchemaEntityWithSchema>().ToList();
}
public async Task<ISchemaEntityWithSchema> FindSchemaAsync(Guid appId, string name)
{
var entity =

4
src/Squidex.Write/Contents/ContentDataCommand.cs

@ -7,14 +7,14 @@
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Squidex.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Write.Contents
{
public class ContentDataCommand : SchemaCommand, IValidatable
{
public JObject Data { get; set; }
public ContentData Data { get; set; }
public void Validate(IList<ValidationError> errors)
{

53
src/Squidex/Config/Identity/SwaggerIdentityUsage.cs

@ -1,53 +0,0 @@
// ==========================================================================
// SwaggerIdentityUsage.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Globalization;
using NSwag;
using NSwag.AspNetCore;
using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors.Security;
namespace Squidex.Config.Identity
{
public static class SwaggerIdentityUsage
{
private const string DescriptionPattern =
@"To retrieve an access token, the client id must make a request to the token url. For example:
$ curl
-X POST '{0}'
-H 'Content-Type: application/x-www-form-urlencoded'
-d 'grant_type=client_credentials&
client_id=[APP_NAME]:[CLIENT_NAME]&
client_secret=[CLIENT_SECRET]'";
public static SwaggerOwinSettings ConfigureIdentity(this SwaggerOwinSettings settings, MyUrlsOptions options)
{
var tokenUrl = options.BuildUrl($"{Constants.IdentityPrefix}/connect/token");
var description = string.Format(CultureInfo.InvariantCulture, DescriptionPattern, tokenUrl);
settings.DocumentProcessors.Add(
new SecurityDefinitionAppender("OAuth2", new SwaggerSecurityScheme
{
TokenUrl = tokenUrl,
Type = SwaggerSecuritySchemeType.OAuth2,
Flow = SwaggerOAuth2Flow.Application,
Scopes = new Dictionary<string, string>
{
{ Constants.ApiScope, "Read and write access to the API" }
},
Description = description
}));
settings.OperationProcessors.Add(new OperationSecurityScopeProcessor("roles"));
return settings;
}
}
}

85
src/Squidex/Config/Swagger/SwaggerServices.cs

@ -0,0 +1,85 @@
// ==========================================================================
// SwaggerServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NJsonSchema;
using NJsonSchema.Generation.TypeMappers;
using NSwag.AspNetCore;
using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors.Security;
using Squidex.Controllers.ContentApi.Generator;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
namespace Squidex.Config.Swagger
{
public static class SwaggerServices
{
public static void AddMySwaggerSettings(this IServiceCollection services)
{
services.AddSingleton(typeof(SwaggerOwinSettings), s =>
{
var options = s.GetService<IOptions<MyUrlsOptions>>().Value;
var settings =
new SwaggerOwinSettings { Title = "Squidex API Specification", IsAspNetCore = false }
.ConfigurePaths()
.ConfigureSchemaSettings()
.ConfigureIdentity(options);
return settings;
});
services.AddTransient<SchemasSwaggerGenerator>();
}
private static SwaggerOwinSettings ConfigureIdentity(this SwaggerOwinSettings settings, MyUrlsOptions urlOptions)
{
settings.DocumentProcessors.Add(
new SecurityDefinitionAppender("OAuth2", SwaggerHelper.CreateOAuthSchema(urlOptions)));
settings.OperationProcessors.Add(new OperationSecurityScopeProcessor("roles"));
return settings;
}
private static SwaggerOwinSettings ConfigurePaths(this SwaggerOwinSettings settings)
{
settings.SwaggerRoute = $"{Constants.ApiPrefix}/swagger/v1/swagger.json";
settings.PostProcess = document =>
{
document.BasePath = Constants.ApiPrefix;
};
settings.MiddlewareBasePath = Constants.ApiPrefix;
return settings;
}
private static SwaggerOwinSettings ConfigureSchemaSettings(this SwaggerOwinSettings settings)
{
settings.DefaultEnumHandling = EnumHandling.String;
settings.DefaultPropertyNameHandling = PropertyNameHandling.CamelCase;
settings.TypeMappers = new List<ITypeMapper>
{
new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String),
new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String)
};
settings.DocumentProcessors.Add(new XmlTagProcessor());
settings.OperationProcessors.Add(new XmlTagProcessor());
settings.OperationProcessors.Add(new XmlResponseTypesProcessor());
return settings;
}
}
}

47
src/Squidex/Config/Swagger/SwaggerUsage.cs

@ -6,16 +6,10 @@
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NJsonSchema;
using NJsonSchema.Generation.TypeMappers;
using NSwag.AspNetCore;
using Squidex.Config.Identity;
using Squidex.Infrastructure;
namespace Squidex.Config.Swagger
{
@ -23,48 +17,9 @@ namespace Squidex.Config.Swagger
{
public static void UseMySwagger(this IApplicationBuilder app)
{
var options = app.ApplicationServices.GetService<IOptions<MyUrlsOptions>>().Value;
var settings =
new SwaggerOwinSettings { Title = "Squidex API Specification", IsAspNetCore = false}
.ConfigurePaths()
.ConfigureSchemaSettings()
.ConfigureIdentity(options);
var settings = app.ApplicationServices.GetService<SwaggerOwinSettings>();
app.UseSwagger(typeof(SwaggerUsage).GetTypeInfo().Assembly, settings);
}
private static SwaggerOwinSettings ConfigurePaths(this SwaggerOwinSettings settings)
{
settings.SwaggerRoute = $"{Constants.ApiPrefix}/swagger/v1/swagger.json";
settings.PostProcess = document =>
{
document.BasePath = Constants.ApiPrefix;
};
settings.MiddlewareBasePath = Constants.ApiPrefix;
return settings;
}
private static SwaggerOwinSettings ConfigureSchemaSettings(this SwaggerOwinSettings settings)
{
settings.DefaultEnumHandling = EnumHandling.String;
settings.DefaultPropertyNameHandling = PropertyNameHandling.CamelCase;
settings.TypeMappers = new List<ITypeMapper>
{
new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String),
new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String)
};
settings.DocumentProcessors.Add(new XmlTagProcessor());
settings.OperationProcessors.Add(new XmlTagProcessor());
settings.OperationProcessors.Add(new XmlResponseTypesProcessor());
return settings;
}
}
}

2
src/Squidex/Controllers/Api/Apps/AppClientsController.cs

@ -97,7 +97,7 @@ namespace Squidex.Controllers.Api.Apps
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="clientId">The id of the client that must be updated.</param>
/// <param name="request">Client object that needs to be added to the app.</param>
/// <param name="request">Client object that needs to be updated.</param>
/// <returns>
/// 201 => Client key generated.
/// 404 => App not found or client not found.

4
src/Squidex/Controllers/Api/Docs/DocsController.cs

@ -18,7 +18,9 @@ namespace Squidex.Controllers.Api.Docs
[Route("docs/")]
public IActionResult Docs()
{
return View();
ViewBag.Specification = "~/swagger/v1/swagger.json";
return View("Docs");
}
}
}

2
src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs

@ -63,7 +63,7 @@ namespace Squidex.Controllers.Api.Schemas.Models
public RefToken LastModifiedBy { get; set; }
/// <summary>
/// The date and time when the schema has been creaed.
/// The date and time when the schema has been created.
/// </summary>
public DateTime Created { get; set; }

2
src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs

@ -50,7 +50,7 @@ namespace Squidex.Controllers.Api.Schemas.Models
public RefToken LastModifiedBy { get; set; }
/// <summary>
/// The date and time when the schema has been creaed.
/// The date and time when the schema has been created.
/// </summary>
public DateTime Created { get; set; }

64
src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs

@ -0,0 +1,64 @@
// ==========================================================================
// ContentSwaggerController.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Controllers.ContentApi.Generator;
using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Read.Schemas.Repositories;
// ReSharper disable UseObjectOrCollectionInitializer
namespace Squidex.Controllers.ContentApi
{
[ApiExceptionFilter]
[SwaggerIgnore]
public class ContentSwaggerController : Controller
{
private readonly ISchemaRepository schemaRepository;
private readonly IAppProvider appProvider;
private readonly SchemasSwaggerGenerator schemasSwaggerGenerator;
public ContentSwaggerController(ISchemaRepository schemaRepository, IAppProvider appProvider, SchemasSwaggerGenerator schemasSwaggerGenerator)
{
this.appProvider = appProvider;
this.schemaRepository = schemaRepository;
this.schemasSwaggerGenerator = schemasSwaggerGenerator;
}
[HttpGet]
[Route("content/{app}/docs/")]
public IActionResult Docs(string app)
{
ViewBag.Specification = $"~/content/{app}/swagger/v1/swagger.json";
return View("Docs");
}
[HttpGet]
[Route("content/{app}/swagger/v1/swagger.json")]
public async Task<IActionResult> GetSwagger(string app)
{
var appEntity = await appProvider.FindAppByNameAsync(app);
if (appEntity == null)
{
return NotFound();
}
var schemas = await schemaRepository.QueryAllWithSchemaAsync(appEntity.Id);
var swaggerDocument = await schemasSwaggerGenerator.Generate(appEntity, schemas);
return Content(swaggerDocument.ToJson(), "application/json");
}
}
}

356
src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs

@ -0,0 +1,356 @@
// ==========================================================================
// SchemasSwaggerGenerator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using NJsonSchema;
using NJsonSchema.Generation;
using NSwag;
using NSwag.AspNetCore;
using NSwag.CodeGeneration.SwaggerGenerators;
using Squidex.Config;
using Squidex.Controllers.Api;
using Squidex.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
using Squidex.Read.Apps;
using Squidex.Read.Schemas.Repositories;
// ReSharper disable SuggestBaseTypeForParameter
// ReSharper disable PrivateFieldCanBeConvertedToLocalVariable
namespace Squidex.Controllers.ContentApi.Generator
{
public sealed class SchemasSwaggerGenerator
{
private const string BodyDescription =
@"The data of the {0} to be created or updated.
Please not that each field is an object with one entry per language.
If the field is not localizable you must use iv (Invariant Language) as a key.
When you change the field to be localizable the value will become the value for the master language, depending what the master language is at this point of time.";
private readonly SwaggerJsonSchemaGenerator schemaGenerator;
private readonly SwaggerDocument document = new SwaggerDocument { Tags = new List<SwaggerTag>() };
private readonly HttpContext context;
private readonly JsonSchemaResolver schemaResolver;
private readonly SwaggerGenerator swaggerGenerator;
private readonly MyUrlsOptions urlOptions;
private HashSet<Language> languages;
private JsonSchema4 errorDtoSchema;
private JsonSchema4 entityCreatedDtoSchema;
private string appBasePath;
private IAppEntity app;
public SchemasSwaggerGenerator(IHttpContextAccessor context, SwaggerOwinSettings swaggerSettings, IOptions<MyUrlsOptions> urlOptions)
{
this.context = context.HttpContext;
this.urlOptions = urlOptions.Value;
schemaGenerator = new SwaggerJsonSchemaGenerator(swaggerSettings);
schemaResolver = new SwaggerSchemaResolver(document, swaggerSettings);
swaggerGenerator = new SwaggerGenerator(schemaGenerator, swaggerSettings, schemaResolver);
}
public async Task<SwaggerDocument> Generate(IAppEntity appEntity, IReadOnlyCollection<ISchemaEntityWithSchema> schemas)
{
app = appEntity;
languages = new HashSet<Language>(appEntity.Languages);
appBasePath = $"/content/{appEntity.Name}";
await GenerateBasicSchemas();
GenerateTitle();
GenerateRequestInfo();
GenerateContentTypes();
GenerateSchemes();
GenerateSchemasOperations(schemas);
GenerateSecurityDefinitions();
GenerateSecurityRequirements();
return document;
}
private void GenerateSchemes()
{
document.Schemes.Add(context.Request.Scheme == "http" ? SwaggerSchema.Http : SwaggerSchema.Https);
}
private void GenerateTitle()
{
document.Host = context.Request.Host.Value ?? string.Empty;
document.BasePath = "/api";
}
private void GenerateRequestInfo()
{
document.Info = new SwaggerInfo
{
Title = $"Suidex API for {app.Name} App"
};
}
private void GenerateContentTypes()
{
document.Consumes = new List<string>
{
"application/json"
};
document.Produces = new List<string>
{
"application/json"
};
}
private void GenerateSecurityDefinitions()
{
document.SecurityDefinitions.Add("OAuth2", SwaggerHelper.CreateOAuthSchema(urlOptions));
}
private async Task GenerateBasicSchemas()
{
var errorType = typeof(ErrorDto);
var errorSchema = JsonObjectTypeDescription.FromType(errorType, new Attribute[0], EnumHandling.String);
errorDtoSchema = await swaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, errorSchema.IsNullable, null);
var entityCreatedType = typeof(EntityCreatedDto);
var entityCreatedSchema = JsonObjectTypeDescription.FromType(entityCreatedType, new Attribute[0], EnumHandling.String);
entityCreatedDtoSchema = await swaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(entityCreatedType, entityCreatedSchema.IsNullable, null);
}
private void GenerateSecurityRequirements()
{
var securityRequirements = new List<SwaggerSecurityRequirement>
{
new SwaggerSecurityRequirement
{
{ "roles", new List<string> { "app-owner", "app-developer", "app-editor" } }
}
};
foreach (var operation in document.Paths.Values.SelectMany(x => x.Values))
{
operation.Security = securityRequirements;
}
}
private void GenerateSchemasOperations(IEnumerable<ISchemaEntityWithSchema> schemas)
{
foreach (var schema in schemas.Select(x => x.Schema))
{
GenerateSchemaOperations(schema);
}
}
private void GenerateSchemaOperations(Schema schema)
{
var schemaName = schema.Properties.Label ?? schema.Name;
document.Tags.Add(
new SwaggerTag
{
Name = schemaName, Description = $"API to managed {schemaName} content elements."
});
var noIdItemOperations =
document.Paths.GetOrAdd($"{appBasePath}/{schema.Name}/", k => new SwaggerOperations());
var idItemOperations =
document.Paths.GetOrAdd($"{appBasePath}/{schema.Name}/{{id}}/", k => new SwaggerOperations());
GenerateSchemaQueryOperation(noIdItemOperations, schema, schemaName);
GenerateSchemaCreateOperation(noIdItemOperations, schema, schemaName);
GenerateSchemaGetOperation(idItemOperations, schema, schemaName);
GenerateSchemaUpdateOperation(idItemOperations, schema, schemaName);
GenerateSchemaDeleteOperation(idItemOperations, schemaName);
foreach (var operation in idItemOperations.Values.Union(noIdItemOperations.Values))
{
operation.Tags = new List<string> { schemaName };
}
foreach (var operation in idItemOperations.Values)
{
operation.Responses.Add("404",
new SwaggerResponse { Description = $"App, schema or {schemaName} not found." });
operation.Parameters.AddPathParameter("id", JsonObjectType.String, $"The id of the {schemaName} (GUID).");
}
}
private void GenerateSchemaQueryOperation(SwaggerOperations operations, Schema schema, string schemaName)
{
var operation = new SwaggerOperation
{
Summary = $"Queries {schemaName} content elements."
};
operation.Parameters.AddQueryParameter("take", JsonObjectType.Number, "The number of elements to take.");
operation.Parameters.AddQueryParameter("skip", JsonObjectType.Number, "The number of elements to skip.");
operation.Parameters.AddQueryParameter("query", JsonObjectType.String, "Optional full text query skip.");
var responseSchema = CreateContentsSchema(schema.BuildSchema(languages, AppendSchema), schemaName, schema.Name);
operation.Responses.Add("200",
new SwaggerResponse { Description = $"{schemaName} content elements retrieved.", Schema = responseSchema });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Querying {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
operations[SwaggerOperationMethod.Get] = operation;
}
private void GenerateSchemaCreateOperation(SwaggerOperations operations, Schema schema, string schemaName)
{
var operation = new SwaggerOperation
{
Summary = $"Create a {schemaName} content element."
};
var bodySchema = AppendSchema($"{schema.Name}Dto", schema.BuildSchema(languages, AppendSchema));
operation.Parameters.AddBodyParameter(bodySchema, "data", string.Format(BodyDescription, schemaName));
operation.Responses.Add("201",
new SwaggerResponse { Description = $"{schemaName} created.", Schema = entityCreatedDtoSchema });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Creating {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
operations[SwaggerOperationMethod.Post] = operation;
}
private void GenerateSchemaGetOperation(SwaggerOperations operations, Schema schema, string schemaName)
{
var operation = new SwaggerOperation
{
Summary = $"Gets a {schemaName} content element"
};
var responseSchema = CreateContentSchema(schema.BuildSchema(languages, AppendSchema), schemaName, schema.Name);
operation.Responses.Add("209",
new SwaggerResponse { Description = $"{schemaName} element found.", Schema = responseSchema });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Retrieving {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
operations[SwaggerOperationMethod.Get] = operation;
}
private void GenerateSchemaUpdateOperation(SwaggerOperations operations, Schema schema, string schemaName)
{
var operation = new SwaggerOperation
{
Summary = $"Update {schemaName} content element."
};
var bodySchema = AppendSchema($"{schema.Name}Dto", schema.BuildSchema(languages, AppendSchema));
operation.Parameters.AddBodyParameter(bodySchema, "data", string.Format(BodyDescription, schemaName));
operation.Responses.Add("204",
new SwaggerResponse { Description = $"{schemaName} element updated." });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Updating {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
operations[SwaggerOperationMethod.Put] = operation;
}
private void GenerateSchemaDeleteOperation(SwaggerOperations operations, string schemaName)
{
var operation = new SwaggerOperation
{
Summary = $"Delete a {schemaName} content element."
};
operation.Responses.Add("204",
new SwaggerResponse { Description = $"{schemaName} element deleted." });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Deleting {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
operations[SwaggerOperationMethod.Delete] = operation;
}
private JsonSchema4 CreateContentsSchema(JsonSchema4 dataSchema, string schemaName, string id)
{
var contentSchema = CreateContentSchema(dataSchema, schemaName, id);
var schema = new JsonSchema4
{
Properties =
{
["total"] = new JsonProperty
{
Type = JsonObjectType.Number, IsRequired = true, Description = $"The total number of {schemaName} content elements."
},
["items"] = new JsonProperty
{
IsRequired = true,
Item = contentSchema,
Type = JsonObjectType.Array,
Description = $"The item of {schemaName} content elements."
}
},
Type = JsonObjectType.Object
};
return schema;
}
private JsonSchema4 CreateContentSchema(JsonSchema4 dataSchema, string schemaName, string id)
{
var CreateProperty =
new Func<string, string, JsonProperty>((d, f) =>
new JsonProperty { Description = d, Format = f, IsRequired = true, Type = JsonObjectType.String });
var dataProperty = new JsonProperty { Type = JsonObjectType.Object, IsRequired = true, Description = "The data of the content element" };
dataProperty.AllOf.Add(dataSchema);
var schema = new JsonSchema4
{
Properties =
{
["id"] = CreateProperty($"The id of the {schemaName}", null),
["data"] = dataProperty,
["created"] = CreateProperty($"The date and time when the {schemaName} content element has been created.", "date-time"),
["createdBy"] = CreateProperty($"The user that has created the {schemaName} content element.", null),
["lastModified"] = CreateProperty($"The date and time when the {schemaName} content element has been modified last.", "date-time"),
["lastModifiedBy"] = CreateProperty($"The user that has updated the {schemaName} content element.", null),
["isPublished"] = new JsonProperty
{
Description = $"Indicates if the {schemaName} content element is publihed.", IsRequired = true, Type = JsonObjectType.Boolean
}
},
Type = JsonObjectType.Object
};
return AppendSchema($"{id}ContentDto", schema);
}
private JsonSchema4 AppendSchema(string name, JsonSchema4 schema)
{
name = char.ToUpperInvariant(name[0]) + name.Substring(1);
return new JsonSchema4 { SchemaReference = document.Definitions.GetOrAdd(name, x => schema) };
}
}
}

56
src/Squidex/Controllers/ContentApi/Models/ContentEntryDto.cs

@ -0,0 +1,56 @@
// ==========================================================================
// ContentEntryDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Controllers.ContentApi.Models
{
public sealed class ContentEntryDto
{
/// <summary>
/// The if of the content element.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The user that has created the content element.
/// </summary>
[Required]
public RefToken CreatedBy { get; set; }
/// <summary>
/// The user that has updated the content element.
/// </summary>
[Required]
public RefToken LastModifiedBy { get; set; }
/// <summary>
/// The data of the content item.
/// </summary>
[Required]
public ContentData Data { get; set; }
/// <summary>
/// The date and time when the content element has been created.
/// </summary>
public DateTime Created { get; set; }
/// <summary>
/// The date and time when the content element has been modified last.
/// </summary>
public DateTime LastModified { get; set; }
/// <summary>
/// Indicates if the content element is publihed.
/// </summary>
public bool IsPublished { get; set; }
}
}

89
src/Squidex/Pipeline/Swagger/SwaggerHelper.cs

@ -0,0 +1,89 @@
// ==========================================================================
// SwaggerHelper.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Globalization;
using NJsonSchema;
using NSwag;
using Squidex.Config;
namespace Squidex.Pipeline.Swagger
{
public static class SwaggerHelper
{
private const string SecurityDescription =
@"To retrieve an access token, the client id must make a request to the token url. For example:
$ curl
-X POST '{0}'
-H 'Content-Type: application/x-www-form-urlencoded'
-d 'grant_type=client_credentials&
client_id=[APP_NAME]:[CLIENT_NAME]&
client_secret=[CLIENT_SECRET]'";
public static SwaggerSecurityScheme CreateOAuthSchema(MyUrlsOptions urlOptions)
{
var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityPrefix}/connect/token");
var description = string.Format(CultureInfo.InvariantCulture, SecurityDescription, tokenUrl);
return
new SwaggerSecurityScheme
{
TokenUrl = tokenUrl,
Type = SwaggerSecuritySchemeType.OAuth2,
Flow = SwaggerOAuth2Flow.Application,
Scopes = new Dictionary<string, string>
{
{ Constants.ApiScope, "Read and write access to the API" }
},
Description = description
};
}
public static void AddQueryParameter(this ICollection<SwaggerParameter> parameters, string name, JsonObjectType type, string description)
{
parameters.Add(
new SwaggerParameter
{
Type = type,
Name = name,
Kind = SwaggerParameterKind.Query,
Description = description
});
}
public static void AddPathParameter(this ICollection<SwaggerParameter> parameters, string name, JsonObjectType type, string description)
{
parameters.Add(
new SwaggerParameter
{
Type = type,
Name = name,
Kind = SwaggerParameterKind.Path,
IsRequired = true,
IsNullableRaw = false,
Description = description
});
}
public static void AddBodyParameter(this ICollection<SwaggerParameter> parameters, JsonSchema4 schema, string name, string description)
{
parameters.Add(
new SwaggerParameter
{
Name = name,
Kind = SwaggerParameterKind.Body,
Schema = schema,
IsRequired = true,
IsNullableRaw = false,
Description = description
});
}
}
}

10
src/Squidex/Startup.cs

@ -58,6 +58,7 @@ namespace Squidex
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMySwaggerSettings();
services.AddMyEventFormatter();
services.AddMyIdentity();
services.AddMyIdentityServer(Environment);
@ -88,7 +89,14 @@ namespace Squidex
builder.RegisterModule<WriteModule>();
builder.Populate(services);
return new AutofacServiceProvider(builder.Build());
var container = builder.Build();
container.Resolve<IApplicationLifetime>().ApplicationStopping.Register(() =>
{
container.Dispose();
});
return new AutofacServiceProvider(container);
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)

2
src/Squidex/Views/Docs/Docs.cshtml → src/Squidex/Views/Shared/Docs.cshtml

@ -20,7 +20,7 @@
</style>
</head>
<body>
<redoc spec-url="@Url.Content("~/swagger/v1/swagger.json")"></redoc>
<redoc spec-url="@Url.Content(ViewBag.Specification)"></redoc>
<script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"></script>
</body>

15
tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs

@ -18,6 +18,7 @@ namespace Squidex.Core.Schemas.Json
public class JsonSerializerTests
{
private static readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
private readonly SchemaJsonSerializer sut;
static JsonSerializerTests()
{
@ -27,6 +28,11 @@ namespace Squidex.Core.Schemas.Json
serializerSettings.SerializationBinder = new TypeNameSerializationBinder();
}
public JsonSerializerTests()
{
sut = new SchemaJsonSerializer(new FieldRegistry(), serializerSettings);
}
[Fact]
public void Should_serialize_and_deserialize_schema()
{
@ -36,16 +42,11 @@ namespace Squidex.Core.Schemas.Json
new StringFieldProperties { Label = "Field1", Pattern = "[0-9]{3}" })).DisableField(1)
.AddOrUpdateField(new NumberField(2, "field2",
new NumberFieldProperties { Hints = "Hints" }))
.AddOrUpdateField(new BooleanField(3, "field2",
.AddOrUpdateField(new BooleanField(3, "field3",
new BooleanFieldProperties())).HideField(2)
.Publish();
var sut = new SchemaJsonSerializer(new FieldRegistry(), serializerSettings);
var token = sut.Serialize(schema);
var deserialized = sut.Deserialize(token);
var deserialized = sut.Deserialize(sut.Serialize(schema));
deserialized.ShouldBeEquivalentTo(schema);
}

4
tests/Squidex.Core.Tests/Schemas/SchemaTests.cs

@ -258,7 +258,9 @@ namespace Squidex.Core.Schemas
.AddOrUpdateField(new NumberField(4, "age",
new NumberFieldProperties()));
var json = schema.BuildSchema(new HashSet<Language>(new [] { Language.GetLanguage("de"), Language.GetLanguage("en") })).ToJson();
var languages = new HashSet<Language>(new[] { Language.GetLanguage("de"), Language.GetLanguage("en") });
var json = schema.BuildSchema(languages, (n, s) => s).ToJson();
Assert.NotNull(json);
}

64
tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs

@ -9,7 +9,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using Squidex.Core.Contents;
using Squidex.Infrastructure;
using Xunit;
@ -25,8 +25,9 @@ namespace Squidex.Core.Schemas
public async Task Should_add_error_if_validating_data_with_unknown_field()
{
var data =
new JObject(
new JProperty("unknown", 1));
ContentData.Empty()
.AddField("unknown",
ContentFieldData.New());
await sut.ValidateAsync(data, errors, languages);
@ -43,8 +44,10 @@ namespace Squidex.Core.Schemas
sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { MaxValue = 100 }));
var data =
new JObject(
new JProperty("my-field", 1000));
ContentData.Empty()
.AddField("my-field",
ContentFieldData.New()
.AddValue(1000));
await sut.ValidateAsync(data, errors, languages);
@ -56,55 +59,58 @@ namespace Squidex.Core.Schemas
}
[Fact]
public async Task Should_add_error_if_validating_data_with_invalid_localizable_field()
public async Task Should_add_error_non_localizable_field_contains_language()
{
sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true, IsLocalizable = true }));
sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties()));
var data =
new JObject();
ContentData.Empty()
.AddField("my-field",
ContentFieldData.New()
.AddValue("es", 1)
.AddValue("it", 1));
await sut.ValidateAsync(data, errors, languages);
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("my-field (de) is required", "my-field"),
new ValidationError("my-field (en) is required", "my-field")
new ValidationError("my-field can only contain a single entry for invariant language (iv)", "my-field")
});
}
[Fact]
public async Task Should_add_error_if_required_field_is_not_in_bag()
public async Task Should_add_error_if_validating_data_with_invalid_localizable_field()
{
sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true }));
sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true, IsLocalizable = true }));
var data =
new JObject();
ContentData.Empty();
await sut.ValidateAsync(data, errors, languages);
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("my-field is required", "my-field")
new ValidationError("my-field (de) is required", "my-field"),
new ValidationError("my-field (en) is required", "my-field")
});
}
[Fact]
public async Task Should_add_error_if_value_is_not_object_for_localizable_field()
public async Task Should_add_error_if_required_field_is_not_in_bag()
{
sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true }));
sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true }));
var data =
new JObject(
new JProperty("my-field", 1));
ContentData.Empty();
await sut.ValidateAsync(data, errors, languages);
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("my-field is localizable and must be an object", "my-field")
new ValidationError("my-field is required", "my-field")
});
}
@ -114,11 +120,11 @@ namespace Squidex.Core.Schemas
sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true }));
var data =
new JObject(
new JProperty("my-field",
new JObject(
new JProperty("de", 1),
new JProperty("xx", 1))));
ContentData.Empty()
.AddField("my-field",
ContentFieldData.New()
.AddValue("de", 1)
.AddValue("xx", 1));
await sut.ValidateAsync(data, errors, languages);
@ -135,11 +141,11 @@ namespace Squidex.Core.Schemas
sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true }));
var data =
new JObject(
new JProperty("my-field",
new JObject(
new JProperty("es", 1),
new JProperty("it", 1))));
ContentData.Empty()
.AddField("my-field",
ContentFieldData.New()
.AddValue("es", 1)
.AddValue("it", 1));
await sut.ValidateAsync(data, errors, languages);

11
tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs

@ -9,7 +9,7 @@
using System;
using System.Threading.Tasks;
using Moq;
using Newtonsoft.Json.Linq;
using Squidex.Core.Contents;
using Squidex.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
@ -35,13 +35,14 @@ namespace Squidex.Write.Contents
private readonly Mock<IAppEntity> appEntity = new Mock<IAppEntity>();
private readonly Guid schemaId = Guid.NewGuid();
private readonly Guid appId = Guid.NewGuid();
private readonly JObject data = new JObject(new JProperty("field", 1));
private readonly ContentData data = ContentData.Empty().AddField("my-field", ContentFieldData.New().AddValue(1));
public ContentCommandHandlerTests()
{
var schema =
Schema.Create("my-schema", new SchemaProperties())
.AddOrUpdateField(new NumberField(1, "field", new NumberFieldProperties { IsRequired = true }));
.AddOrUpdateField(new NumberField(1, "field",
new NumberFieldProperties { IsRequired = true }));
content = new ContentDomainObject(Id, 0);
@ -57,7 +58,7 @@ namespace Squidex.Write.Contents
[Fact]
public async Task Create_should_throw_exception_if_data_is_not_valid()
{
var command = new CreateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = new JObject() };
var command = new CreateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = ContentData.Empty() };
var context = new CommandContext(command);
await TestCreate(content, async _ =>
@ -85,7 +86,7 @@ namespace Squidex.Write.Contents
{
CreateContent();
var command = new UpdateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = new JObject() };
var command = new UpdateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = ContentData.Empty() };
var context = new CommandContext(command);
await TestUpdate(content, async _ =>

4
tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs

@ -9,7 +9,7 @@
using System;
using System.Linq;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using Squidex.Core.Contents;
using Squidex.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
@ -26,7 +26,7 @@ namespace Squidex.Write.Contents
{
private readonly Guid appId = Guid.NewGuid();
private readonly ContentDomainObject sut;
private readonly JObject data = new JObject();
private readonly ContentData data = ContentData.Empty();
public ContentDomainObjectTests()
{

Loading…
Cancel
Save