Browse Source

OData Support

pull/1/head
Sebastian 9 years ago
parent
commit
cff5838d7e
  1. 2
      src/Squidex.Core/Contents/ContentData.cs
  2. 2
      src/Squidex.Core/Contents/ContentFieldData.cs
  3. 7
      src/Squidex.Core/Schemas/BooleanField.cs
  4. 27
      src/Squidex.Core/Schemas/Field.cs
  5. 7
      src/Squidex.Core/Schemas/NumberField.cs
  6. 23
      src/Squidex.Core/Schemas/Schema.cs
  7. 17
      src/Squidex.Core/Schemas/StringField.cs
  8. 1
      src/Squidex.Core/Schemas/Validators/PatternValidator.cs
  9. 1
      src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs
  10. 1
      src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs
  11. 1
      src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs
  12. 1
      src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs
  13. 1
      src/Squidex.Infrastructure/CollectionExtensions.cs
  14. 2
      src/Squidex.Infrastructure/Dispatching/ActionContextDispatcherFactory.cs
  15. 2
      src/Squidex.Infrastructure/Dispatching/ActionDispatcherFactory.cs
  16. 2
      src/Squidex.Infrastructure/Dispatching/FuncContextDispatcherFactory.cs
  17. 2
      src/Squidex.Infrastructure/Dispatching/FuncDispatcherFactory.cs
  18. 1
      src/Squidex.Infrastructure/Guard.cs
  19. 1
      src/Squidex.Infrastructure/Reflection/SimpleMapper.cs
  20. 1
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  21. 5
      src/Squidex.Read/Contents/Repositories/IContentRepository.cs
  22. 1
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  23. 1
      src/Squidex.Store.MongoDb/Contents/MongoContentEntity.cs
  24. 127
      src/Squidex.Store.MongoDb/Contents/MongoContentRepository.cs
  25. 32
      src/Squidex.Store.MongoDb/Contents/Visitors/ConstantVisitor.cs
  26. 39
      src/Squidex.Store.MongoDb/Contents/Visitors/FilterBuilder.cs
  27. 95
      src/Squidex.Store.MongoDb/Contents/Visitors/FilterVisitor.cs
  28. 72
      src/Squidex.Store.MongoDb/Contents/Visitors/FindExtensions.cs
  29. 63
      src/Squidex.Store.MongoDb/Contents/Visitors/PropertyVisitor.cs
  30. 58
      src/Squidex.Store.MongoDb/Contents/Visitors/SchemaExtensions.cs
  31. 66
      src/Squidex.Store.MongoDb/Contents/Visitors/SortBuilder.cs
  32. 1
      src/Squidex.Write/Apps/AppContributors.cs
  33. 12
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  34. 9
      src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs

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

@ -39,7 +39,7 @@ namespace Squidex.Core.Contents
foreach (var fieldValue in this) foreach (var fieldValue in this)
{ {
var fieldId = 0L; long fieldId;
Field field; Field field;

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

@ -22,7 +22,7 @@ namespace Squidex.Core.Contents
public ContentFieldData SetValue(JToken value) public ContentFieldData SetValue(JToken value)
{ {
this["iv"] = value; this[Language.Invariant.Iso2Code] = value;
return this; return this;
} }

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

@ -7,6 +7,8 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Library;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NJsonSchema; using NJsonSchema;
using Squidex.Core.Schemas.Validators; using Squidex.Core.Schemas.Validators;
@ -37,5 +39,10 @@ namespace Squidex.Core.Schemas
{ {
jsonProperty.Type = JsonObjectType.Boolean; jsonProperty.Type = JsonObjectType.Boolean;
} }
protected override IEdmTypeReference CreateEdmType()
{
return EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, !Properties.IsRequired);
}
} }
} }

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

@ -9,6 +9,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Library;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NJsonSchema; using NJsonSchema;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -129,6 +131,27 @@ namespace Squidex.Core.Schemas
return Clone<Field>(clone => clone.name = newName); return Clone<Field>(clone => clone.name = newName);
} }
public void AddToEdmType(EdmStructuredType edmType, IEnumerable<Language> languages, string schemaName, Func<EdmComplexType, EdmComplexType> typeResolver)
{
Guard.NotNull(edmType, nameof(edmType));
Guard.NotNull(languages, nameof(languages));
Guard.NotNull(typeResolver, nameof(typeResolver));
if (!RawProperties.IsLocalizable)
{
languages = new[] { Language.Invariant };
}
var languageType = typeResolver(new EdmComplexType("Squidex", $"{schemaName}_{Name}_Property"));
foreach (var language in languages)
{
languageType.AddStructuralProperty(language.Iso2Code, CreateEdmType());
}
edmType.AddStructuralProperty(Name, new EdmComplexTypeReference(languageType, false));
}
public void AddToSchema(JsonSchema4 schema, IEnumerable<Language> languages, string schemaName, Func<string, JsonSchema4, JsonSchema4> schemaResolver) public void AddToSchema(JsonSchema4 schema, IEnumerable<Language> languages, string schemaName, Func<string, JsonSchema4, JsonSchema4> schemaResolver)
{ {
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
@ -152,7 +175,7 @@ namespace Squidex.Core.Schemas
languagesObject.Properties.Add(language.Iso2Code, languageProperty); languagesObject.Properties.Add(language.Iso2Code, languageProperty);
} }
languagesProperty.AllOf.Add(schemaResolver($"{schemaName}{Name}Property", languagesObject)); languagesProperty.AllOf.Add(schemaResolver($"{schemaName}_{Name}_Property", languagesObject));
schema.Properties.Add(Name, languagesProperty); schema.Properties.Add(Name, languagesProperty);
} }
@ -180,6 +203,8 @@ namespace Squidex.Core.Schemas
protected abstract IEnumerable<IValidator> CreateValidators(); protected abstract IEnumerable<IValidator> CreateValidators();
protected abstract IEdmTypeReference CreateEdmType();
protected abstract void PrepareJsonSchema(JsonProperty jsonProperty); protected abstract void PrepareJsonSchema(JsonProperty jsonProperty);
protected abstract object ConvertValue(JToken value); protected abstract object ConvertValue(JToken value);

7
src/Squidex.Core/Schemas/NumberField.cs

@ -8,6 +8,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Library;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NJsonSchema; using NJsonSchema;
using Squidex.Core.Schemas.Validators; using Squidex.Core.Schemas.Validators;
@ -54,6 +56,11 @@ namespace Squidex.Core.Schemas
} }
} }
protected override IEdmTypeReference CreateEdmType()
{
return EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, !Properties.IsRequired);
}
protected override object ConvertValue(JToken value) protected override object ConvertValue(JToken value)
{ {
return (double?)value; return (double?)value;

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

@ -11,11 +11,11 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.Edm.Library;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NJsonSchema; using NJsonSchema;
using Squidex.Core.Contents; using Squidex.Core.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression // ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -173,6 +173,21 @@ namespace Squidex.Core.Schemas
return AddOrUpdateField(newField); return AddOrUpdateField(newField);
} }
public EdmComplexType BuildEdmType(HashSet<Language> languages, Func<EdmComplexType, EdmComplexType> typeResolver)
{
Guard.NotEmpty(languages, nameof(languages));
Guard.NotNull(typeResolver, nameof(typeResolver));
var edmType = new EdmComplexType("Squidex", Name);
foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden))
{
field.AddToEdmType(edmType, languages, Name, typeResolver);
}
return edmType;
}
public JsonSchema4 BuildSchema(HashSet<Language> languages, Func<string, JsonSchema4, JsonSchema4> schemaResolver) public JsonSchema4 BuildSchema(HashSet<Language> languages, Func<string, JsonSchema4, JsonSchema4> schemaResolver)
{ {
Guard.NotEmpty(languages, nameof(languages)); Guard.NotEmpty(languages, nameof(languages));
@ -233,12 +248,12 @@ namespace Squidex.Core.Schemas
} }
else else
{ {
if (fieldData.Keys.Any(x => x != "iv")) if (fieldData.Keys.Any(x => x != Language.Invariant.Iso2Code))
{ {
fieldErrors.Add($"{field.Name} can only contain a single entry for invariant language (iv)"); fieldErrors.Add($"{field.Name} can only contain a single entry for invariant language ({Language.Invariant.Iso2Code})");
} }
var value = fieldData.GetOrCreate("iv", k => JValue.CreateNull()); var value = fieldData.GetOrCreate(Language.Invariant.Iso2Code, k => JValue.CreateNull());
await field.ValidateAsync(value, fieldErrors); await field.ValidateAsync(value, fieldErrors);
} }

17
src/Squidex.Core/Schemas/StringField.cs

@ -9,6 +9,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Library;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NJsonSchema; using NJsonSchema;
using Squidex.Core.Schemas.Validators; using Squidex.Core.Schemas.Validators;
@ -45,11 +47,6 @@ namespace Squidex.Core.Schemas
} }
} }
protected override object ConvertValue(JToken value)
{
return value.ToString();
}
protected override void PrepareJsonSchema(JsonProperty jsonProperty) protected override void PrepareJsonSchema(JsonProperty jsonProperty)
{ {
jsonProperty.Type = JsonObjectType.String; jsonProperty.Type = JsonObjectType.String;
@ -62,5 +59,15 @@ namespace Squidex.Core.Schemas
jsonProperty.EnumerationNames = new Collection<string>(Properties.AllowedValues); jsonProperty.EnumerationNames = new Collection<string>(Properties.AllowedValues);
} }
} }
protected override IEdmTypeReference CreateEdmType()
{
return EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, !Properties.IsRequired);
}
protected override object ConvertValue(JToken value)
{
return value.ToString();
}
} }
} }

1
src/Squidex.Core/Schemas/Validators/PatternValidator.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression // ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable InvertIf // ReSharper disable InvertIf

1
src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs

@ -8,7 +8,6 @@
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers; using MongoDB.Bson.Serialization.Serializers;
// ReSharper disable InvertIf // ReSharper disable InvertIf
namespace Squidex.Infrastructure.MongoDb namespace Squidex.Infrastructure.MongoDb

1
src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs

@ -8,7 +8,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
// ReSharper disable InvertIf // ReSharper disable InvertIf
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands

1
src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs

@ -8,7 +8,6 @@
using System; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace Squidex.Infrastructure.CQRS.Events namespace Squidex.Infrastructure.CQRS.Events

1
src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs

@ -12,7 +12,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NodaTime; using NodaTime;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression // ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable InvertIf // ReSharper disable InvertIf

1
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -9,7 +9,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
// ReSharper disable InvertIf // ReSharper disable InvertIf
// ReSharper disable LoopCanBeConvertedToQuery // ReSharper disable LoopCanBeConvertedToQuery

2
src/Squidex.Infrastructure/Dispatching/ActionContextDispatcherFactory.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Reflection; using System.Reflection;
// ReSharper disable UnusedMember.Local
namespace Squidex.Infrastructure.Dispatching namespace Squidex.Infrastructure.Dispatching
{ {
@ -27,7 +28,6 @@ namespace Squidex.Infrastructure.Dispatching
return new Tuple<Type, Action<TTarget, object, TContext>>(inputType, (Action<TTarget, object, TContext>)handler); return new Tuple<Type, Action<TTarget, object, TContext>>(inputType, (Action<TTarget, object, TContext>)handler);
} }
// ReSharper disable once UnusedMember.Local
private static Action<TTarget, object, TContext> Factory<TTarget, TIn, TContext>(MethodInfo methodInfo) private static Action<TTarget, object, TContext> Factory<TTarget, TIn, TContext>(MethodInfo methodInfo)
{ {
var type = typeof(Action<TTarget, TIn, TContext>); var type = typeof(Action<TTarget, TIn, TContext>);

2
src/Squidex.Infrastructure/Dispatching/ActionDispatcherFactory.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Reflection; using System.Reflection;
// ReSharper disable UnusedMember.Local
namespace Squidex.Infrastructure.Dispatching namespace Squidex.Infrastructure.Dispatching
{ {
@ -27,7 +28,6 @@ namespace Squidex.Infrastructure.Dispatching
return new Tuple<Type, Action<T, object>>(inputType, (Action<T, object>)handler); return new Tuple<Type, Action<T, object>>(inputType, (Action<T, object>)handler);
} }
// ReSharper disable once UnusedMember.Local
private static Action<TTarget, object> Factory<TTarget, TIn>(MethodInfo methodInfo) private static Action<TTarget, object> Factory<TTarget, TIn>(MethodInfo methodInfo)
{ {
var type = typeof(Action<TTarget, TIn>); var type = typeof(Action<TTarget, TIn>);

2
src/Squidex.Infrastructure/Dispatching/FuncContextDispatcherFactory.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Reflection; using System.Reflection;
// ReSharper disable UnusedMember.Local
namespace Squidex.Infrastructure.Dispatching namespace Squidex.Infrastructure.Dispatching
{ {
@ -27,7 +28,6 @@ namespace Squidex.Infrastructure.Dispatching
return new Tuple<Type, Func<TTarget, object, TContext, TOut>>(inputType, (Func<TTarget, object, TContext, TOut>)handler); return new Tuple<Type, Func<TTarget, object, TContext, TOut>>(inputType, (Func<TTarget, object, TContext, TOut>)handler);
} }
// ReSharper disable once UnusedMember.Local
private static Func<TTarget, object, TContext, TOut> Factory<TTarget, TIn, TContext, TOut>(MethodInfo methodInfo) private static Func<TTarget, object, TContext, TOut> Factory<TTarget, TIn, TContext, TOut>(MethodInfo methodInfo)
{ {
var type = typeof(Func<TTarget, TIn, TContext, TOut>); var type = typeof(Func<TTarget, TIn, TContext, TOut>);

2
src/Squidex.Infrastructure/Dispatching/FuncDispatcherFactory.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Reflection; using System.Reflection;
// ReSharper disable once UnusedMember.Local
namespace Squidex.Infrastructure.Dispatching namespace Squidex.Infrastructure.Dispatching
{ {
@ -27,7 +28,6 @@ namespace Squidex.Infrastructure.Dispatching
return new Tuple<Type, Func<TTarget, object, TReturn>>(inputType, (Func<TTarget, object, TReturn>)handler); return new Tuple<Type, Func<TTarget, object, TReturn>>(inputType, (Func<TTarget, object, TReturn>)handler);
} }
// ReSharper disable once UnusedMember.Local
private static Func<TTarget, object, TReturn> Factory<TTarget, TIn, TReturn>(MethodInfo methodInfo) private static Func<TTarget, object, TReturn> Factory<TTarget, TIn, TReturn>(MethodInfo methodInfo)
{ {
var type = typeof(Func<TTarget, TIn, TReturn>); var type = typeof(Func<TTarget, TIn, TReturn>);

1
src/Squidex.Infrastructure/Guard.cs

@ -12,7 +12,6 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
// ReSharper disable InvertIf // ReSharper disable InvertIf
namespace Squidex.Infrastructure namespace Squidex.Infrastructure

1
src/Squidex.Infrastructure/Reflection/SimpleMapper.cs

@ -10,7 +10,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
// ReSharper disable StaticMemberInGenericType // ReSharper disable StaticMemberInGenericType
namespace Squidex.Infrastructure.Reflection namespace Squidex.Infrastructure.Reflection

1
src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs

@ -15,7 +15,6 @@ using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Repositories;
using Squidex.Read.Utils; using Squidex.Read.Utils;
// ReSharper disable InvertIf // ReSharper disable InvertIf
namespace Squidex.Read.Apps.Services.Implementations namespace Squidex.Read.Apps.Services.Implementations

5
src/Squidex.Read/Contents/Repositories/IContentRepository.cs

@ -9,14 +9,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Read.Contents.Repositories namespace Squidex.Read.Contents.Repositories
{ {
public interface IContentRepository public interface IContentRepository
{ {
Task<List<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, int? take, int? skip, string query); Task<List<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages);
Task<long> CountAsync(Guid schemaId, bool nonPublished, string query); Task<long> CountAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages);
Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id); Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id);
} }

1
src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -16,7 +16,6 @@ using Squidex.Infrastructure.CQRS.Events;
using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Utils; using Squidex.Read.Utils;
using Squidex.Events; using Squidex.Events;
// ReSharper disable InvertIf // ReSharper disable InvertIf
namespace Squidex.Read.Schemas.Services.Implementations namespace Squidex.Read.Schemas.Services.Implementations

1
src/Squidex.Store.MongoDb/Contents/MongoContentEntity.cs

@ -18,7 +18,6 @@ using Squidex.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Read.Contents; using Squidex.Read.Contents;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression // ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable InvertIf // ReSharper disable InvertIf

127
src/Squidex.Store.MongoDb/Contents/MongoContentRepository.cs

@ -25,6 +25,7 @@ using Squidex.Infrastructure.Reflection;
using Squidex.Read.Contents; using Squidex.Read.Contents;
using Squidex.Read.Contents.Repositories; using Squidex.Read.Contents.Repositories;
using Squidex.Read.Schemas.Services; using Squidex.Read.Schemas.Services;
using Squidex.Store.MongoDb.Contents.Visitors;
using Squidex.Store.MongoDb.Utils; using Squidex.Store.MongoDb.Utils;
namespace Squidex.Store.MongoDb.Contents namespace Squidex.Store.MongoDb.Contents
@ -35,22 +36,6 @@ namespace Squidex.Store.MongoDb.Contents
private readonly IMongoDatabase database; private readonly IMongoDatabase database;
private readonly ISchemaProvider schemaProvider; private readonly ISchemaProvider schemaProvider;
protected ProjectionDefinitionBuilder<MongoContentEntity> Projection
{
get
{
return Builders<MongoContentEntity>.Projection;
}
}
protected SortDefinitionBuilder<MongoContentEntity> Sort
{
get
{
return Builders<MongoContentEntity>.Sort;
}
}
protected UpdateDefinitionBuilder<MongoContentEntity> Update protected UpdateDefinitionBuilder<MongoContentEntity> Update
{ {
get get
@ -59,14 +44,6 @@ namespace Squidex.Store.MongoDb.Contents
} }
} }
protected FilterDefinitionBuilder<MongoContentEntity> Filter
{
get
{
return Builders<MongoContentEntity>.Filter;
}
}
protected IndexKeysDefinitionBuilder<MongoContentEntity> IndexKeys protected IndexKeysDefinitionBuilder<MongoContentEntity> IndexKeys
{ {
get get
@ -104,96 +81,60 @@ namespace Squidex.Store.MongoDb.Contents
} }
} }
public async Task<List<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, int? take, int? skip, string query) public async Task<List<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages)
{
var cursor = BuildQuery(schemaId, nonPublished, query);
if (take.HasValue)
{
cursor.Limit(take.Value);
}
if (skip.HasValue)
{ {
cursor.Skip(skip.Value); List<IContentEntity> result = null;
}
cursor.SortByDescending(x => x.LastModified);
var schemaEntity = await schemaProvider.FindSchemaByIdAsync(schemaId); await ForSchemaAsync(schemaId, async (collection, schema) =>
if (schemaEntity == null)
{ {
return new List<IContentEntity>(); var parser = schema.ParseQuery(languages, odataQuery);
} var cursor = collection.Find(parser, schema, nonPublished).Take(parser).Skip(parser).Sort(parser, schema);
var entities = await cursor.ToListAsync(); var entities = await cursor.ToListAsync();
foreach (var entity in entities) foreach (var entity in entities)
{ {
entity.ParseData(schemaEntity.Schema); entity.ParseData(schema);
}
return entities.OfType<IContentEntity>().ToList();
} }
public Task<long> CountAsync(Guid schemaId, bool nonPublished, string query) result = entities.OfType<IContentEntity>().ToList();
{ });
var cursor = BuildQuery(schemaId, nonPublished, query);
return cursor.CountAsync(); return result;
} }
private IFindFluent<MongoContentEntity, MongoContentEntity> BuildQuery(Guid schemaId, bool nonPublished, string query) public async Task<long> CountAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.IsDeleted, false)
};
if (!string.IsNullOrWhiteSpace(query))
{ {
filters.Add(Filter.Text(query, "en")); var result = 0L;
}
if (!nonPublished) await ForSchemaAsync(schemaId, async (collection, schema) =>
{ {
filters.Add(Filter.Eq(x => x.IsPublished, false)); var parser = schema.ParseQuery(languages, odataQuery);
} var cursor = collection.Find(parser, schema, nonPublished);
var collection = GetCollection(schemaId); result = await cursor.CountAsync();
});
var cursor = collection.Find(Filter.And(filters));
return cursor; return result;
} }
public async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id) public async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id)
{ {
var collection = GetCollection(schemaId); MongoContentEntity result = null;
var entity = await collection.Find(x => x.Id == id).FirstOrDefaultAsync();
if (entity == null)
{
return null;
}
var schemaEntity = await schemaProvider.FindSchemaByIdAsync(schemaId); await ForSchemaAsync(schemaId, async (collection, schema) =>
if (schemaEntity == null)
{ {
return null; result = await collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
entity.ParseData(schemaEntity.Schema); result?.ParseData(schema);
});
return entity; return result;
} }
protected Task On(ContentCreated @event, EnvelopeHeaders headers) protected Task On(ContentCreated @event, EnvelopeHeaders headers)
{ {
return ForSchemaAsync(headers, (collection, schema) => return ForSchemaAsync(headers.SchemaId(), (collection, schema) =>
{ {
return collection.CreateAsync(headers, x => return collection.CreateAsync(headers, x =>
{ {
@ -206,7 +147,7 @@ namespace Squidex.Store.MongoDb.Contents
protected Task On(ContentUpdated @event, EnvelopeHeaders headers) protected Task On(ContentUpdated @event, EnvelopeHeaders headers)
{ {
return ForSchemaAsync(headers, (collection, schema) => return ForSchemaAsync(headers.SchemaId(), (collection, schema) =>
{ {
return collection.UpdateAsync(headers, x => return collection.UpdateAsync(headers, x =>
{ {
@ -217,7 +158,7 @@ namespace Squidex.Store.MongoDb.Contents
protected Task On(ContentPublished @event, EnvelopeHeaders headers) protected Task On(ContentPublished @event, EnvelopeHeaders headers)
{ {
return ForSchemaAsync(headers, collection => return ForSchemaAsync(headers.SchemaId(), collection =>
{ {
return collection.UpdateAsync(headers, x => return collection.UpdateAsync(headers, x =>
{ {
@ -228,7 +169,7 @@ namespace Squidex.Store.MongoDb.Contents
protected Task On(ContentUnpublished @event, EnvelopeHeaders headers) protected Task On(ContentUnpublished @event, EnvelopeHeaders headers)
{ {
return ForSchemaAsync(headers, collection => return ForSchemaAsync(headers.SchemaId(), collection =>
{ {
return collection.UpdateAsync(headers, x => return collection.UpdateAsync(headers, x =>
{ {
@ -239,7 +180,7 @@ namespace Squidex.Store.MongoDb.Contents
protected Task On(ContentDeleted @event, EnvelopeHeaders headers) protected Task On(ContentDeleted @event, EnvelopeHeaders headers)
{ {
return ForSchemaAsync(headers, collection => return ForSchemaAsync(headers.SchemaId(), collection =>
{ {
return collection.UpdateAsync(headers, x => return collection.UpdateAsync(headers, x =>
{ {
@ -267,11 +208,11 @@ namespace Squidex.Store.MongoDb.Contents
return this.DispatchActionAsync(@event.Payload, @event.Headers); return this.DispatchActionAsync(@event.Payload, @event.Headers);
} }
private async Task ForSchemaAsync(EnvelopeHeaders headers, Func<IMongoCollection<MongoContentEntity>, Schema, Task> action) private async Task ForSchemaAsync(Guid schemaId, Func<IMongoCollection<MongoContentEntity>, Schema, Task> action)
{ {
var collection = GetCollection(headers.SchemaId()); var collection = GetCollection(schemaId);
var schemaEntity = await schemaProvider.FindSchemaByIdAsync(headers.SchemaId()); var schemaEntity = await schemaProvider.FindSchemaByIdAsync(schemaId);
if (schemaEntity == null) if (schemaEntity == null)
{ {
@ -281,9 +222,9 @@ namespace Squidex.Store.MongoDb.Contents
await action(collection, schemaEntity.Schema); await action(collection, schemaEntity.Schema);
} }
private async Task ForSchemaAsync(EnvelopeHeaders headers, Func<IMongoCollection<MongoContentEntity>, Task> action) private async Task ForSchemaAsync(Guid schemaId, Func<IMongoCollection<MongoContentEntity>, Task> action)
{ {
var collection = GetCollection(headers.SchemaId()); var collection = GetCollection(schemaId);
await action(collection); await action(collection);
} }

32
src/Squidex.Store.MongoDb/Contents/Visitors/ConstantVisitor.cs

@ -0,0 +1,32 @@
// ==========================================================================
// ConstantVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.OData.Core.UriParser.Semantic;
using Microsoft.OData.Core.UriParser.Visitors;
namespace Squidex.Store.MongoDb.Contents.Visitors
{
public sealed class ConstantVisitor : QueryNodeVisitor<object>
{
private static readonly ConstantVisitor Instance = new ConstantVisitor();
private ConstantVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override object Visit(ConstantNode nodeIn)
{
return nodeIn.Value;
}
}
}

39
src/Squidex.Store.MongoDb/Contents/Visitors/FilterBuilder.cs

@ -0,0 +1,39 @@
// ==========================================================================
// FilterBuilder.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.OData.Core.UriParser;
using MongoDB.Driver;
using Squidex.Core.Schemas;
// ReSharper disable ConvertIfStatementToReturnStatement
namespace Squidex.Store.MongoDb.Contents.Visitors
{
public static class FilterBuilder
{
private static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter;
public static FilterDefinition<MongoContentEntity> Build(ODataUriParser query, Schema schema)
{
var search = query.ParseSearch();
if (search != null)
{
return Filter.Text(ConstantVisitor.Visit(search.Expression).ToString());
}
var filter = query.ParseFilter();
if (filter != null)
{
return FilterVisitor.Visit(filter.Expression, schema);
}
return null;
}
}
}

95
src/Squidex.Store.MongoDb/Contents/Visitors/FilterVisitor.cs

@ -0,0 +1,95 @@
// ==========================================================================
// FilterVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Microsoft.OData.Core.UriParser.Semantic;
using Microsoft.OData.Core.UriParser.TreeNodeKinds;
using Microsoft.OData.Core.UriParser.Visitors;
using MongoDB.Driver;
using Squidex.Core.Schemas;
// ReSharper disable SwitchStatementMissingSomeCases
// ReSharper disable ConvertIfStatementToSwitchStatement
namespace Squidex.Store.MongoDb.Contents.Visitors
{
public class FilterVisitor : QueryNodeVisitor<FilterDefinition<MongoContentEntity>>
{
private static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter;
private readonly Schema schema;
private FilterVisitor(Schema schema)
{
this.schema = schema;
}
public static FilterDefinition<MongoContentEntity> Visit(QueryNode node, Schema schema)
{
var visitor = new FilterVisitor(schema);
return node.Accept(visitor);
}
public override FilterDefinition<MongoContentEntity> Visit(UnaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == UnaryOperatorKind.Not)
{
return Filter.Not(nodeIn.Operand.Accept(this));
}
throw new NotSupportedException();
}
public override FilterDefinition<MongoContentEntity> Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return Filter.And(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Or)
{
return Filter.Or(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual)
{
return Filter.Ne(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal)
{
return Filter.Eq(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan)
{
return Filter.Lt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{
return Filter.Lte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
{
return Filter.Gt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
{
return Filter.Gte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
throw new NotSupportedException();
}
private FieldDefinition<MongoContentEntity, object> BuildFieldDefinition(QueryNode nodeIn)
{
return PropertyVisitor.Visit(nodeIn, schema);
}
private static object BuildValue(QueryNode nodeIn)
{
return ConstantVisitor.Visit(nodeIn);
}
}
}

72
src/Squidex.Store.MongoDb/Contents/Visitors/FindExtensions.cs

@ -0,0 +1,72 @@
// ==========================================================================
// FindExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Microsoft.OData.Core.UriParser;
using MongoDB.Driver;
using Squidex.Core.Schemas;
namespace Squidex.Store.MongoDb.Contents.Visitors
{
public static class FindExtensions
{
private static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter;
public static IFindFluent<MongoContentEntity, MongoContentEntity> Sort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query, Schema schema)
{
return cursor.Sort(SortBuilder.BuildSort(query, schema));
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> Take(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query)
{
var top = query.ParseTop();
if (top.HasValue)
{
cursor = cursor.Limit((int)top.Value);
}
return cursor;
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> Skip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query)
{
var skip = query.ParseSkip();
if (skip.HasValue)
{
cursor = cursor.Skip((int)skip.Value);
}
return cursor;
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> Find(this IMongoCollection<MongoContentEntity> cursor, ODataUriParser query, Schema schema, bool nonPublished)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.IsDeleted, false)
};
if (!nonPublished)
{
filters.Add(Filter.Eq(x => x.IsPublished, false));
}
var filter = FilterBuilder.Build(query, schema);
if (filter != null)
{
filters.Add(filter);
}
return cursor.Find(Filter.And(filters));
}
}
}

63
src/Squidex.Store.MongoDb/Contents/Visitors/PropertyVisitor.cs

@ -0,0 +1,63 @@
// ==========================================================================
// PropertyVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.OData.Core.UriParser.Semantic;
using Microsoft.OData.Core.UriParser.Visitors;
using MongoDB.Driver;
using Squidex.Core.Schemas;
// ReSharper disable InvertIf
// ReSharper disable RedundantIfElseBlock
namespace Squidex.Store.MongoDb.Contents.Visitors
{
public sealed class PropertyVisitor : QueryNodeVisitor<ImmutableList<string>>
{
private static readonly PropertyVisitor Instance = new PropertyVisitor();
public static StringFieldDefinition<MongoContentEntity, object> Visit(QueryNode node, Schema schema)
{
var propertyNames = node.Accept(Instance).ToArray();
if (propertyNames.Length == 3)
{
Field field;
if (!schema.FieldsByName.TryGetValue(propertyNames[1], out field))
{
throw new NotSupportedException();
}
propertyNames[1] = field.Id.ToString();
}
var propertyName = string.Join(".", propertyNames);
return new StringFieldDefinition<MongoContentEntity, object>(propertyName);
}
public override ImmutableList<string> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override ImmutableList<string> Visit(SingleValuePropertyAccessNode nodeIn)
{
if (nodeIn.Source is SingleValuePropertyAccessNode)
{
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name);
}
else
{
return ImmutableList.Create(nodeIn.Property.Name);
}
}
}
}

58
src/Squidex.Store.MongoDb/Contents/Visitors/SchemaExtensions.cs

@ -0,0 +1,58 @@
// ==========================================================================
// SchemaExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Microsoft.OData.Core.UriParser;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Library;
using Squidex.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Store.MongoDb.Contents.Visitors
{
public static class SchemaExtensions
{
public static EdmModel BuildEdmModel(this Schema schema, HashSet<Language> languages)
{
var model = new EdmModel();
var container = new EdmEntityContainer("Squidex", "Container");
var schemaType = schema.BuildEdmType(languages, x =>
{
model.AddElement(x);
return x;
});
var entityType = new EdmEntityType("Squidex", schema.Name);
entityType.AddStructuralProperty("Data", new EdmComplexTypeReference(schemaType, false));
entityType.AddStructuralProperty("Created", EdmPrimitiveTypeKind.Date);
entityType.AddStructuralProperty("CreatedBy", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("LastModified", EdmPrimitiveTypeKind.Date);
entityType.AddStructuralProperty("LastModifiedBy", EdmPrimitiveTypeKind.String);
model.AddElement(container);
model.AddElement(schemaType);
model.AddElement(entityType);
container.AddEntitySet($"{schema.Name}_Set", entityType);
return model;
}
public static ODataUriParser ParseQuery(this Schema schema, HashSet<Language> languages, string query)
{
var model = schema.BuildEdmModel(languages);
var parser = new ODataUriParser(model, new Uri($"{schema.Name}_Set?{query}", UriKind.Relative));
return parser;
}
}
}

66
src/Squidex.Store.MongoDb/Contents/Visitors/SortBuilder.cs

@ -0,0 +1,66 @@
// ==========================================================================
// SortBuilder.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Microsoft.OData.Core.UriParser;
using Microsoft.OData.Core.UriParser.Semantic;
using MongoDB.Driver;
using Squidex.Core.Schemas;
// ReSharper disable RedundantIfElseBlock
// ReSharper disable ConvertIfStatementToReturnStatement
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
namespace Squidex.Store.MongoDb.Contents.Visitors
{
public static class SortBuilder
{
private static readonly SortDefinitionBuilder<MongoContentEntity> Sort = Builders<MongoContentEntity>.Sort;
public static SortDefinition<MongoContentEntity> BuildSort(ODataUriParser query, Schema schema)
{
var orderBy = query.ParseOrderBy();
if (orderBy != null)
{
var sorts = new List<SortDefinition<MongoContentEntity>>();
while (orderBy != null)
{
sorts.Add(OrderBy(orderBy, schema));
orderBy = orderBy.ThenBy;
}
if (sorts.Count > 1)
{
return Sort.Combine(sorts);
}
else
{
return sorts[0];
}
}
else
{
return Sort.Descending(x => x.LastModified);
}
}
public static SortDefinition<MongoContentEntity> OrderBy(OrderByClause clause, Schema schema)
{
if (clause.Direction == OrderByDirection.Ascending)
{
return Sort.Ascending(PropertyVisitor.Visit(clause.Expression, schema));
}
else
{
return Sort.Descending(PropertyVisitor.Visit(clause.Expression, schema));
}
}
}
}

1
src/Squidex.Write/Apps/AppContributors.cs

@ -11,7 +11,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Core.Apps; using Squidex.Core.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
// ReSharper disable InvertIf // ReSharper disable InvertIf
namespace Squidex.Write.Apps namespace Squidex.Write.Apps

12
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -12,10 +12,10 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Squidex.Controllers.Api; using Squidex.Controllers.Api;
using Squidex.Controllers.ContentApi.Models; using Squidex.Controllers.ContentApi.Models;
using Squidex.Core.Contents; using Squidex.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
@ -42,7 +42,7 @@ namespace Squidex.Controllers.ContentApi
[HttpGet] [HttpGet]
[Route("content/{app}/{name}")] [Route("content/{app}/{name}")]
public async Task<IActionResult> GetContents(string name, [FromQuery] string query = null, [FromQuery] int? take = null, [FromQuery] int? skip = null, [FromQuery] bool nonPublished = false, [FromQuery] bool hidden = false) public async Task<IActionResult> GetContents(string name, [FromQuery] bool nonPublished = false, [FromQuery] bool hidden = false)
{ {
var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name); var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name);
@ -51,8 +51,12 @@ namespace Squidex.Controllers.ContentApi
return NotFound(); return NotFound();
} }
var taskForContents = contentRepository.QueryAsync(schemaEntity.Id, nonPublished, take, skip, query); var languages = new HashSet<Language>(App.Languages);
var taskForCount = contentRepository.CountAsync(schemaEntity.Id, nonPublished, query);
var query = Request.QueryString.ToString();
var taskForContents = contentRepository.QueryAsync(schemaEntity.Id, nonPublished, query, languages);
var taskForCount = contentRepository.CountAsync(schemaEntity.Id, nonPublished, query, languages);
await Task.WhenAll(taskForContents, taskForCount); await Task.WhenAll(taskForContents, taskForCount);

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

@ -25,7 +25,6 @@ using Squidex.Pipeline.Swagger;
using Squidex.Read.Apps; using Squidex.Read.Apps;
using Squidex.Read.Schemas; using Squidex.Read.Schemas;
// ReSharper disable InvertIf // ReSharper disable InvertIf
// ReSharper disable SuggestBaseTypeForParameter // ReSharper disable SuggestBaseTypeForParameter
// ReSharper disable PrivateFieldCanBeConvertedToLocalVariable // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable
@ -203,10 +202,10 @@ When you change the field to be localizable the value will become the value for
{ {
operation.Summary = $"Queries {schemaName} content elements."; operation.Summary = $"Queries {schemaName} content elements.";
operation.Parameters.AddQueryParameter("take", JsonObjectType.Number, "The number of elements to take."); operation.Parameters.AddQueryParameter("$top", JsonObjectType.Number, "The number of elements to take.");
operation.Parameters.AddQueryParameter("skip", JsonObjectType.Number, "The number of elements to skip."); operation.Parameters.AddQueryParameter("$skip", JsonObjectType.Number, "The number of elements to skip.");
operation.Parameters.AddQueryParameter("$filter", JsonObjectType.String, "Optional filter.");
operation.Parameters.AddQueryParameter("query", JsonObjectType.String, "Optional full text query skip."); operation.Parameters.AddQueryParameter("$search", JsonObjectType.String, "Optional full text query skip.");
var responseSchema = CreateContentsSchema(schema.BuildSchema(languages, AppendSchema), schemaName, schema.Name); var responseSchema = CreateContentsSchema(schema.BuildSchema(languages, AppendSchema), schemaName, schema.Name);

Loading…
Cancel
Save