diff --git a/CHANGELOG.md b/CHANGELOG.md index 30180d367..88998f810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,27 @@ # Changelog +## v1.4.1 - 2018-05-02 + +### Bugfixes + +* **Orleans**: Remove orleans dashboard from 8080. + +## v1.4.0 - 2018-05-02 + +### Features + +* **UI**: Big refactorings and UI improvements. +* **Actions**: New log formatter with placeholder for user infos. +* **Actions**: Azure Queue action. +* **Actions**: Algolia action. +* **Actions**: Fastly action. +* **Backup**: Backup all your data to an archive. + ## v1.3.0 - 2018-02-17 ### Features -* **Actions**: ElasticSearch action +* **Actions**: ElasticSearch action. ### Refactorings diff --git a/Dockerfile b/Dockerfile index ed342ba9d..cde0085b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ FROM microsoft/aspnetcore:2.0.3-jessie # Default AspNetCore directory WORKDIR /app -# Copy from nuild stage +# Copy from build stage COPY --from=builder /out/ . EXPOSE 80 diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs index 963832075..c83cf9626 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using Newtonsoft.Json.Linq; using NodaTime; namespace Squidex.Domain.Apps.Core.Rules @@ -28,6 +29,6 @@ namespace Squidex.Domain.Apps.Core.Rules public Instant Expires { get; set; } - public RuleJobData ActionData { get; set; } + public JObject ActionData { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs index 3820bd6c5..606f90b3a 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public string Placeholder { get; set; } + public string EditorUrl { get; set; } + public abstract T Accept(IFieldPropertiesVisitor visitor); public abstract Field CreateField(long id, string name, Partitioning partitioning); diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs index 49193c29d..cec57ddae 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs @@ -11,17 +11,32 @@ using Algolia.Search; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +#pragma warning disable SA1649 // File name must match first type name + namespace Squidex.Domain.Apps.Core.HandleRules.Actions { - public sealed class AlgoliaActionHandler : RuleActionHandler + public sealed class AlgoliaJob + { + public string AppId { get; set; } + public string ApiKey { get; set; } + + public string ContentId { get; set; } + + public string IndexName { get; set; } + + public JObject Content { get; set; } + } + + public sealed class AlgoliaActionHandler : RuleActionHandler { + private const string DescriptionIgnore = "Ignore"; + private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients; private readonly RuleEventFormatter formatter; @@ -39,20 +54,20 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions }); } - protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, AlgoliaAction action) + protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(Envelope @event, string eventName, AlgoliaAction action) { - var ruleDescription = string.Empty; - var ruleData = new RuleJobData - { - ["AppId"] = action.AppId, - ["ApiKey"] = action.ApiKey - }; - if (@event.Payload is ContentEvent contentEvent) { - ruleData["ContentId"] = contentEvent.ContentId.ToString(); - ruleData["Operation"] = "Upsert"; - ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event); + var contentId = contentEvent.ContentId.ToString(); + + var ruleDescription = string.Empty; + var ruleJob = new AlgoliaJob + { + AppId = action.AppId, + ApiKey = action.ApiKey, + ContentId = contentId, + IndexName = await formatter.FormatStringAsync(action.IndexName, @event) + }; var timestamp = @event.Headers.Timestamp().ToString(); @@ -62,8 +77,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; - ruleData["Content"] = new JObject( - new JProperty("id", contentEvent.ContentId), + ruleJob.Content = new JObject( + new JProperty("objectID", contentId), + new JProperty("id", contentId), new JProperty("created", timestamp), new JProperty("createdBy", created.Actor.ToString()), new JProperty("lastModified", timestamp), @@ -77,7 +93,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { ruleDescription = $"Update entry in Algolia index: {action.IndexName}"; - ruleData["Content"] = new JObject( + ruleJob.Content = new JObject( + new JProperty("objectID", contentId), new JProperty("lastModified", timestamp), new JProperty("lastModifiedBy", updated.Actor.ToString()), new JProperty("data", formatter.ToRouteData(updated.Data))); @@ -88,7 +105,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { ruleDescription = $"Update entry in Algolia index: {action.IndexName}"; - ruleData["Content"] = new JObject( + ruleJob.Content = new JObject( + new JProperty("objectID", contentId), new JProperty("lastModified", timestamp), new JProperty("lastModifiedBy", statusChanged.Actor.ToString()), new JProperty("status", statusChanged.Status.ToString())); @@ -99,55 +117,38 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { ruleDescription = $"Delete entry from Algolia index: {action.IndexName}"; - ruleData["Content"] = new JObject(); - ruleData["Operation"] = "Delete"; break; } } + + return (ruleDescription, ruleJob); } - return (ruleDescription, ruleData); + return (DescriptionIgnore, new AlgoliaJob()); } - public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(AlgoliaJob job) { - if (!job.TryGetValue("Operation", out var operationToken)) + if (string.IsNullOrWhiteSpace(job.AppId)) { return (null, new InvalidOperationException("The action cannot handle this event.")); } - var appId = job["AppId"].Value(); - var apiKey = job["ApiKey"].Value(); - var indexName = job["IndexName"].Value(); - - var index = clients.GetClient((appId, apiKey, indexName)); - - var operation = operationToken.Value(); - var content = job["Content"].Value(); - var contentId = job["ContentId"].Value(); + var index = clients.GetClient((job.AppId, job.ApiKey, job.IndexName)); try { - switch (operation) + if (job.Content != null) { - case "Upsert": - { - content["objectID"] = contentId; - - var response = await index.PartialUpdateObjectAsync(content); - - return (response.ToString(Formatting.Indented), null); - } - - case "Delete": - { - var response = await index.DeleteObjectAsync(contentId); + var response = await index.PartialUpdateObjectAsync(job.Content); - return (response.ToString(Formatting.Indented), null); - } + return (response.ToString(Formatting.Indented), null); + } + else + { + var response = await index.DeleteObjectAsync(job.ContentId); - default: - return (null, null); + return (response.ToString(Formatting.Indented), null); } } catch (AlgoliaException ex) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs index 56dd3154a..2b0e50b91 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs @@ -11,15 +11,34 @@ using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Queue; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +#pragma warning disable SA1649 // File name must match first type name + namespace Squidex.Domain.Apps.Core.HandleRules.Actions { - public sealed class AzureQueueActionHandler : RuleActionHandler + public sealed class AzureQueueJob + { + public string QueueConnectionString { get; set; } + public string QueueName { get; set; } + + public string MessageBodyV2 { get; set; } + + public JObject MessageBody { get; set; } + + public string Body + { + get + { + return MessageBodyV2 ?? MessageBody.ToString(Formatting.Indented); + } + } + } + + public sealed class AzureQueueActionHandler : RuleActionHandler { private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients; private readonly RuleEventFormatter formatter; @@ -41,31 +60,28 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions }); } - protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, AzureQueueAction action) + protected override async Task<(string Description, AzureQueueJob Data)> CreateJobAsync(Envelope @event, string eventName, AzureQueueAction action) { - var body = formatter.ToRouteData(@event, eventName); + var body = formatter.ToRouteData(@event, eventName).ToString(Formatting.Indented); - var ruleDescription = $"Send event to azure queue '{action.Queue}'"; - var ruleData = new RuleJobData + var queueName = await formatter.FormatStringAsync(action.Queue, @event); + + var ruleDescription = $"Send AzureQueueJob to azure queue '{action.Queue}'"; + var ruleJob = new AzureQueueJob { - ["QueueConnectionString"] = action.ConnectionString, - ["QueueName"] = action.Queue, - ["MessageBody"] = body + QueueConnectionString = action.ConnectionString, + QueueName = queueName, + MessageBodyV2 = body, }; - return (ruleDescription, ruleData); + return (ruleDescription, ruleJob); } - public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(AzureQueueJob job) { - var queueConnectionString = job["QueueConnectionString"].Value(); - var queueName = job["QueueName"].Value(); - - var queue = clients.GetClient((queueConnectionString, queueName)); - - var messageBody = job["MessageBody"].ToString(Formatting.Indented); + var queue = clients.GetClient((job.QueueConnectionString, job.QueueName)); - await queue.AddMessageAsync(new CloudQueueMessage(messageBody)); + await queue.AddMessageAsync(new CloudQueueMessage(job.Body)); return ("Completed", null); } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs index a65813fce..94df18789 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs @@ -10,17 +10,37 @@ using System.Threading.Tasks; using Elasticsearch.Net; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +#pragma warning disable SA1649 // File name must match first type name + namespace Squidex.Domain.Apps.Core.HandleRules.Actions { - public sealed class ElasticSearchActionHandler : RuleActionHandler + public sealed class ElasticSearchJob + { + public string Host { get; set; } + + public string Username { get; set; } + public string Password { get; set; } + + public string ContentId { get; set; } + + public string IndexName { get; set; } + public string IndexType { get; set; } + + public string Operation { get; set; } + + public JObject Content { get; set; } + } + + public sealed class ElasticSearchActionHandler : RuleActionHandler { + private const string DescriptionIgnore = "Ignore"; + private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients; private readonly RuleEventFormatter formatter; @@ -43,37 +63,40 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions }); } - protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, ElasticSearchAction action) + protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(Envelope @event, string eventName, ElasticSearchAction action) { - var ruleDescription = string.Empty; - var ruleData = new RuleJobData - { - ["Host"] = action.Host, - ["Username"] = action.Username, - ["Password"] = action.Password - }; - if (@event.Payload is ContentEvent contentEvent) { - ruleData["ContentId"] = contentEvent.ContentId.ToString(); - ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event); - ruleData["IndexType"] = formatter.FormatString(action.IndexType, @event); + var contentId = contentEvent.ContentId.ToString(); + + var ruleDescription = string.Empty; + var ruleJob = new ElasticSearchJob + { + Host = action.Host.ToString(), + Username = action.Username, + Password = action.Password, + ContentId = contentId, + IndexName = await formatter.FormatStringAsync(action.IndexName, @event), + IndexType = await formatter.FormatStringAsync(action.IndexType, @event), + }; var timestamp = @event.Headers.Timestamp().ToString(); + var actor = @event.Payload.Actor.ToString(); + switch (@event.Payload) { case ContentCreated created: { ruleDescription = $"Add entry to ES index: {action.IndexName}"; - ruleData["Operation"] = "Create"; - ruleData["Content"] = new JObject( - new JProperty("id", contentEvent.ContentId), + ruleJob.Operation = "Create"; + ruleJob.Content = new JObject( + new JProperty("id", contentId), new JProperty("created", timestamp), - new JProperty("createdBy", created.Actor.ToString()), + new JProperty("createdBy", actor), new JProperty("lastModified", timestamp), - new JProperty("lastModifiedBy", created.Actor.ToString()), + new JProperty("lastModifiedBy", actor), new JProperty("status", Status.Draft.ToString()), new JProperty("data", formatter.ToRouteData(created.Data))); break; @@ -83,10 +106,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { ruleDescription = $"Update entry in ES index: {action.IndexName}"; - ruleData["Operation"] = "Update"; - ruleData["Content"] = new JObject( + ruleJob.Operation = "Update"; + ruleJob.Content = new JObject( new JProperty("lastModified", timestamp), - new JProperty("lastModifiedBy", updated.Actor.ToString()), + new JProperty("lastModifiedBy", actor), new JProperty("data", formatter.ToRouteData(updated.Data))); break; } @@ -95,10 +118,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { ruleDescription = $"Update entry in ES index: {action.IndexName}"; - ruleData["Operation"] = "Update"; - ruleData["Content"] = new JObject( + ruleJob.Operation = "Update"; + ruleJob.Content = new JObject( new JProperty("lastModified", timestamp), - new JProperty("lastModifiedBy", statusChanged.Actor.ToString()), + new JProperty("lastModifiedBy", actor), new JProperty("status", statusChanged.Status.ToString())); break; } @@ -107,62 +130,49 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { ruleDescription = $"Delete entry from ES index: {action.IndexName}"; - ruleData["Operation"] = "Delete"; - ruleData["Content"] = new JObject(); + ruleJob.Operation = "Delete"; break; } } } - return (ruleDescription, ruleData); + return (DescriptionIgnore, new ElasticSearchJob()); } - public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(ElasticSearchJob job) { - if (!job.TryGetValue("Operation", out var operationToken)) + if (string.IsNullOrWhiteSpace(job.Operation)) { return (null, new InvalidOperationException("The action cannot handle this event.")); } - var host = new Uri(job["Host"].Value(), UriKind.Absolute); - - var username = job["Username"].Value(); - var password = job["Password"].Value(); - - var client = clients.GetClient((host, username, password)); - - var indexName = job["IndexName"].Value(); - var indexType = job["IndexType"].Value(); - - var operation = operationToken.Value(); - var content = job["Content"].Value(); - var contentId = job["ContentId"].Value(); + var client = clients.GetClient((new Uri(job.Host, UriKind.Absolute), job.Username, job.Password)); try { - switch (operation) + switch (job.Operation) { case "Create": { - var doc = content.ToString(); + var doc = job.Content.ToString(); - var response = await client.IndexAsync(indexName, indexType, contentId, doc); + var response = await client.IndexAsync(job.IndexName, job.IndexType, job.ContentId, doc); return (response.Body, response.OriginalException); } case "Update": { - var doc = new JObject(new JProperty("doc", content)).ToString(); + var doc = new JObject(new JProperty("doc", job.Content)).ToString(); - var response = await client.UpdateAsync(indexName, indexType, contentId, doc); + var response = await client.UpdateAsync(job.IndexName, job.IndexType, job.ContentId, doc); return (response.Body, response.OriginalException); } case "Delete": { - var response = await client.DeleteAsync(indexName, indexType, contentId); + var response = await client.DeleteAsync(job.IndexName, job.IndexType, job.ContentId); return (response.Body, response.OriginalException); } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs index 5a8a252bd..8d76518cc 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs @@ -6,45 +6,55 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Http; +#pragma warning disable SA1649 // File name must match first type name + namespace Squidex.Domain.Apps.Core.HandleRules.Actions { - public sealed class FastlyActionHandler : RuleActionHandler + public sealed class FastlyJob { - protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, FastlyAction action) - { - var ruleDescription = "Purge key in fastly"; - var ruleData = new RuleJobData - { - ["FastlyApiKey"] = action.ApiKey, - ["FastlyServiceID"] = action.ServiceId - }; + public string FastlyApiKey { get; set; } + public string FastlyServiceID { get; set; } + + public string Key { get; set; } + } + + public sealed class FastlyActionHandler : RuleActionHandler + { + private const string Description = "Purge key in fastly"; + private const string DescriptionIgnore = "Ignore"; + protected override Task<(string Description, FastlyJob Data)> CreateJobAsync(Envelope @event, string eventName, FastlyAction action) + { if (@event.Headers.Contains(CommonHeaders.AggregateId)) { - ruleData["Key"] = @event.Headers.AggregateId().ToString(); + var ruleJob = new FastlyJob + { + Key = @event.Headers.AggregateId().ToString(), + FastlyApiKey = action.ApiKey, + FastlyServiceID = action.ServiceId + }; + + return Task.FromResult((Description, ruleJob)); } - return (ruleDescription, ruleData); + return Task.FromResult((DescriptionIgnore, new FastlyJob())); } - public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(FastlyJob job) { - if (!job.TryGetValue("Key", out var keyToken)) + if (string.IsNullOrWhiteSpace(job.Key)) { return (null, new InvalidOperationException("The action cannot handle this event.")); } - var requestMsg = BuildRequest(job, keyToken.Value()); + var requestMsg = BuildRequest(job); HttpResponseMessage response = null; @@ -59,29 +69,18 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions } catch (Exception ex) { - if (requestMsg != null) - { - var requestDump = DumpFormatter.BuildDump(requestMsg, response, null, ex.ToString(), TimeSpan.Zero, false); + var requestDump = DumpFormatter.BuildDump(requestMsg, response, null, ex.ToString(), TimeSpan.Zero, false); - return (requestDump, ex); - } - else - { - var requestDump = ex.ToString(); - - return (requestDump, ex); - } + return (requestDump, ex); } } - private static HttpRequestMessage BuildRequest(Dictionary job, string key) + private static HttpRequestMessage BuildRequest(FastlyJob job) { - var serviceId = job["FastlyServiceID"].Value(); - - var requestUrl = $"https://api.fastly.com/service/{serviceId}/purge/{key}"; + var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}"; var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); - request.Headers.Add("Fastly-Key", job["FastlyApiKey"].Value()); + request.Headers.Add("Fastly-Key", job.FastlyApiKey); return request; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs index 4c341df54..c9ec58e14 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs @@ -6,23 +6,41 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Http; +#pragma warning disable SA1649 // File name must match first type name + namespace Squidex.Domain.Apps.Core.HandleRules.Actions { - public sealed class SlackActionHandler : RuleActionHandler + public sealed class SlackJob { + public string RequestUrl { get; set; } + public string RequestBodyV2 { get; set; } + + public JObject RequestBody { get; set; } + + public string Body + { + get + { + return RequestBodyV2 ?? RequestBody.ToString(Formatting.Indented); + } + } + } + + public sealed class SlackActionHandler : RuleActionHandler + { + private const string Description = "Send message to slack"; + private readonly RuleEventFormatter formatter; public SlackActionHandler(RuleEventFormatter formatter) @@ -32,61 +50,51 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions this.formatter = formatter; } - protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, SlackAction action) + protected override async Task<(string Description, SlackJob Data)> CreateJobAsync(Envelope @event, string eventName, SlackAction action) { - var body = CreatePayload(@event, action.Text); + var body = await CreatePayloadAsync(@event, action.Text); - var ruleDescription = "Send message to slack"; - var ruleData = new RuleJobData + var ruleJob = new SlackJob { - ["RequestUrl"] = action.WebhookUrl, - ["RequestBody"] = body + RequestUrl = action.WebhookUrl.ToString(), + RequestBodyV2 = body.ToString(Formatting.Indented), }; - return (ruleDescription, ruleData); + return (Description, ruleJob); } - private JObject CreatePayload(Envelope @event, string text) + private async Task CreatePayloadAsync(Envelope @event, string text) { - return new JObject(new JProperty("text", formatter.FormatString(text, @event))); + return new JObject(new JProperty("text", await formatter.FormatStringAsync(text, @event))); } - public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(SlackJob job) { - var requestBody = job["RequestBody"].ToString(Formatting.Indented); - var requestMsg = BuildRequest(job, requestBody); + var requestBody = job.Body; + var requestMessage = BuildRequest(job, requestBody); HttpResponseMessage response = null; try { - response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg); + response = await HttpClientPool.GetHttpClient().SendAsync(requestMessage); var responseString = await response.Content.ReadAsStringAsync(); - var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false); + var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, responseString, TimeSpan.Zero, false); return (requestDump, null); } catch (Exception ex) { - if (requestMsg != null) - { - var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, ex.ToString(), TimeSpan.Zero, false); - - return (requestDump, ex); - } - else - { - throw; - } + var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, ex.ToString(), TimeSpan.Zero, false); + + return (requestDump, ex); } } - private static HttpRequestMessage BuildRequest(Dictionary job, string requestBody) + private static HttpRequestMessage BuildRequest(SlackJob job, string requestBody) { - var requestUrl = job["RequestUrl"].Value(); - - var request = new HttpRequestMessage(HttpMethod.Post, requestUrl) + var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) { Content = new StringContent(requestBody, Encoding.UTF8, "application/json") }; diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs index bac9ef7fd..316dc125b 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs @@ -6,22 +6,39 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Http; +#pragma warning disable SA1649 // File name must match first type name + namespace Squidex.Domain.Apps.Core.HandleRules.Actions { - public sealed class WebhookActionHandler : RuleActionHandler + public sealed class WebhookJob + { + public string RequestUrl { get; set; } + public string RequestSignature { get; set; } + public string RequestBodyV2 { get; set; } + + public JObject RequestBody { get; set; } + + public string Body + { + get + { + return RequestBodyV2 ?? RequestBody.ToString(Formatting.Indented); + } + } + } + + public sealed class WebhookActionHandler : RuleActionHandler { private readonly RuleEventFormatter formatter; @@ -32,36 +49,34 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions this.formatter = formatter; } - protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, WebhookAction action) + protected override async Task<(string Description, WebhookJob Data)> CreateJobAsync(Envelope @event, string eventName, WebhookAction action) { - var body = formatter.ToRouteData(@event, eventName); - - var signature = $"{body.ToString(Formatting.Indented)}{action.SharedSecret}".Sha256Base64(); + var body = formatter.ToRouteData(@event, eventName).ToString(Formatting.Indented); var ruleDescription = $"Send event to webhook '{action.Url}'"; - var ruleData = new RuleJobData + var ruleJob = new WebhookJob { - ["RequestUrl"] = action.Url, - ["RequestBody"] = body, - ["RequestSignature"] = signature + RequestUrl = await formatter.FormatStringAsync(action.Url.ToString(), @event), + RequestSignature = $"{body}{action.SharedSecret}".Sha256Base64(), + RequestBodyV2 = body }; - return (ruleDescription, ruleData); + return (ruleDescription, ruleJob); } - public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(WebhookJob job) { - var requestBody = job["RequestBody"].ToString(Formatting.Indented); - var requestMsg = BuildRequest(job, requestBody); + var requestBody = job.Body; + var requestMessage = BuildRequest(job, requestBody); HttpResponseMessage response = null; try { - response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg); + response = await HttpClientPool.GetHttpClient().SendAsync(requestMessage); var responseString = await response.Content.ReadAsStringAsync(); - var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false); + var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, responseString, TimeSpan.Zero, false); Exception ex = null; @@ -74,30 +89,20 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions } catch (Exception ex) { - if (requestMsg != null) - { - var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, ex.ToString(), TimeSpan.Zero, false); + var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, ex.ToString(), TimeSpan.Zero, false); - return (requestDump, ex); - } - else - { - throw; - } + return (requestDump, ex); } } - private static HttpRequestMessage BuildRequest(Dictionary job, string requestBody) + private static HttpRequestMessage BuildRequest(WebhookJob job, string requestBody) { - var requestUrl = job["RequestUrl"].Value(); - var requestSig = job["RequestSignature"].Value(); - - var request = new HttpRequestMessage(HttpMethod.Post, requestUrl) + var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) { Content = new StringContent(requestBody, Encoding.UTF8, "application/json") }; - request.Headers.Add("X-Signature", requestSig); + request.Headers.Add("X-Signature", job.RequestSignature); request.Headers.Add("User-Agent", "Squidex Webhook"); return request; diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs index 75f7e7fbe..ec0fdf198 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs @@ -7,6 +7,7 @@ using System; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; @@ -17,8 +18,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules { Type ActionType { get; } - (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, RuleAction action); + Task<(string Description, JObject Data)> CreateJobAsync(Envelope @event, string eventName, RuleAction action); - Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData data); + Task<(string Dump, Exception Exception)> ExecuteJobAsync(JObject data); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs index 22a41360d..ea1ead7f5 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs @@ -7,26 +7,36 @@ using System; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Core.HandleRules { - public abstract class RuleActionHandler : IRuleActionHandler where T : RuleAction + public abstract class RuleActionHandler : IRuleActionHandler where TAction : RuleAction { Type IRuleActionHandler.ActionType { - get { return typeof(T); } + get { return typeof(TAction); } } - (string Description, RuleJobData Data) IRuleActionHandler.CreateJob(Envelope @event, string eventName, RuleAction action) + async Task<(string Description, JObject Data)> IRuleActionHandler.CreateJobAsync(Envelope @event, string eventName, RuleAction action) { - return CreateJob(@event, eventName, (T)action); + var (description, data) = await CreateJobAsync(@event, eventName, (TAction)action); + + return (description, JObject.FromObject(data)); + } + + async Task<(string Dump, Exception Exception)> IRuleActionHandler.ExecuteJobAsync(JObject data) + { + var typedData = data.ToObject(); + + return await ExecuteJobAsync(typedData); } - protected abstract (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, T action); + protected abstract Task<(string Description, TData Data)> CreateJobAsync(Envelope @event, string eventName, TAction action); - public abstract Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job); + protected abstract Task<(string Dump, Exception Exception)> ExecuteJobAsync(TData job); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 3a4587954..6401a786b 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -9,6 +9,7 @@ using System; using System.Globalization; using System.Text; using System.Text.RegularExpressions; +using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -67,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules new JProperty("timestamp", @event.Headers.Timestamp().ToString())); } - public virtual string FormatString(string text, Envelope @event) + public async virtual Task FormatStringAsync(string text, Envelope @event) { var sb = new StringBuilder(text); @@ -96,7 +97,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules sb.Replace(ContentUrlPlaceholder, urlGenerator.GenerateContentUIUrl(@event.Payload.AppId, contentEvent.SchemaId, contentEvent.ContentId)); } - FormatUserInfo(@event, sb); + await FormatUserInfoAsync(@event, sb); + FormatContentAction(@event, sb); var result = sb.ToString(); @@ -114,7 +116,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return result; } - private void FormatUserInfo(Envelope @event, StringBuilder sb) + private async Task FormatUserInfoAsync(Envelope @event, StringBuilder sb) { var text = sb.ToString(); @@ -131,7 +133,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules } else { - var user = FindUser(actor); + var user = await FindUserAsync(actor); if (user != null) { @@ -222,17 +224,17 @@ namespace Squidex.Domain.Apps.Core.HandleRules }); } - private IUser FindUser(RefToken actor) + private Task FindUserAsync(RefToken actor) { var key = $"RuleEventFormatter_Users_${actor.Identifier}"; - return memoryCache.GetOrCreate(key, x => + return memoryCache.GetOrCreateAsync(key, async x => { x.AbsoluteExpirationRelativeToNow = UserCacheDuration; try { - return userResolver.FindByIdOrEmailAsync(actor.Identifier).Result; + return await userResolver.FindByIdOrEmailAsync(actor.Identifier); } catch { diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index ecac1cf7a..019399f0b 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -11,6 +11,7 @@ using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using NodaTime; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Events; @@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules this.clock = clock; } - public virtual RuleJob CreateJob(Rule rule, Envelope @event) + public virtual async Task CreateJobAsync(Rule rule, Envelope @event) { Guard.NotNull(rule, nameof(rule)); Guard.NotNull(@event, nameof(@event)); @@ -80,7 +81,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules var now = clock.GetCurrentInstant(); var actionName = typeNameRegistry.GetName(actionType); - var actionData = actionHandler.CreateJob(appEventEnvelope, eventName, rule.Action); + var actionData = await actionHandler.CreateJobAsync(appEventEnvelope, eventName, rule.Action); var eventTime = @event.Headers.Contains(CommonHeaders.Timestamp) ? @@ -113,7 +114,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return job; } - public virtual async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, RuleJobData job) + public virtual async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, JObject job) { try { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs deleted file mode 100644 index 58a61860d..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Apps -{ - public sealed class MongoAppEntity : IVersionedEntity - { - [BsonId] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } - - [BsonElement] - [BsonRequired] - [BsonJson] - public AppState State { get; set; } - - [BsonElement] - [BsonRequired] - public long Version { get; set; } - - [BsonElement] - [BsonRequired] - public string Name { get; set; } - - [BsonElement] - [BsonRequired] - public string[] UserIds { get; set; } - - [BsonElement] - [BsonIgnoreIfDefault] - public bool IsArchived { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs deleted file mode 100644 index 0b68080ab..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Apps.Repositories; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Apps -{ - public sealed partial class MongoAppRepository : MongoRepositoryBase, IAppRepository - { - public MongoAppRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "States_Apps"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection) - { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name)); - } - - public async Task> QueryAppIdsAsync() - { - var appEntities = - await Collection.Find(new BsonDocument()).Only(x => x.Id) - .ToListAsync(); - - return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - - public async Task> QueryUserAppIdsAsync(string userId) - { - var appEntities = - await Collection.Find(x => x.UserIds.Contains(userId) && x.IsArchived != true).Only(x => x.Id) - .ToListAsync(); - - return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - - public async Task FindAppIdByNameAsync(string name) - { - var appEntity = - await Collection.Find(x => x.Name == name && x.IsArchived != true).Only(x => x.Id) - .FirstOrDefaultAsync(); - - return appEntity != null ? Guid.Parse(appEntity["_id"].AsString) : Guid.Empty; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository_SnapshotStore.cs deleted file mode 100644 index da46ed5d2..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository_SnapshotStore.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Apps -{ - public sealed partial class MongoAppRepository : ISnapshotStore - { - public async Task<(AppState Value, long Version)> ReadAsync(Guid key) - { - var existing = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (existing.State, existing.Version); - } - - return (null, EtagVersion.NotFound); - } - - public Task WriteAsync(Guid key, AppState value, long oldVersion, long newVersion) - { - return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u - .Set(x => x.Name, value.Name) - .Set(x => x.State, value) - .Set(x => x.UserIds, value.Contributors.Keys.ToArray()) - .Set(x => x.IsArchived, value.IsArchived)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index a4056d0a7..92633415d 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -17,6 +17,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public sealed partial class MongoAssetRepository : ISnapshotStore { + Task ISnapshotStore.ReadAllAsync(Func callback) + { + throw new NotSupportedException(); + } + public async Task<(AssetState Value, long Version)> ReadAsync(Guid key) { var existing = diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index b70738f7e..9ce18c582 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -25,11 +25,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonId] [BsonRequired] - [BsonElement] - public string DocumentId { get; set; } - - [BsonRequired] - [BsonElement("id")] [BsonRepresentation(BsonType.String)] public Guid Id { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index f081f7fc3..7a6a48be7 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -20,6 +20,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { public partial class MongoContentRepository : ISnapshotStore { + Task ISnapshotStore.ReadAllAsync(Func callback) + { + throw new NotSupportedException(); + } + public async Task<(ContentState Value, long Version)> ReadAsync(Guid key) { var contentEntity = @@ -49,14 +54,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents var idData = value.Data?.ToIdModel(schema.SchemaDef, true); - var id = key.ToString(); - var document = SimpleMapper.Map(value, new MongoContentEntity { AppIdId = value.AppId.Id, SchemaIdId = value.SchemaId.Id, IsDeleted = value.IsDeleted, - DocumentId = key.ToString(), DataText = idData?.ToFullText(), DataByIds = idData, PendingDataByIds = value.PendingData?.ToIdModel(schema.SchemaDef, true), @@ -67,14 +69,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents try { - await Collection.ReplaceOneAsync(x => x.DocumentId == id && x.Version == oldVersion, document, Upsert); + await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, document, Upsert); } catch (MongoWriteException ex) { if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await Collection.Find(x => x.DocumentId == id).Only(x => x.DocumentId, x => x.Version) + await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version) .FirstOrDefaultAsync(); if (existingVersion != null) @@ -87,8 +89,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents throw; } } - - document.DocumentId = $"{key}_{newVersion}"; } private async Task GetSchemaAsync(Guid appId, Guid schemaId) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs index 31dad2699..156a32822 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs @@ -11,8 +11,6 @@ using NodaTime; using Squidex.Domain.Apps.Entities.History; using Squidex.Infrastructure; -#pragma warning disable RECS0029 // Warns about property or indexer setters and event adders or removers that do not use the value parameter - namespace Squidex.Domain.Apps.Entities.MongoDb.History { internal sealed class ParsedHistoryEvent : IHistoryEventEntity @@ -25,6 +23,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History get { return inner.Id; } } + public Guid EventId + { + get { return inner.Id; } + } + public Instant Created { get { return inner.Created; } @@ -40,11 +43,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History get { return inner.Actor; } } - public Guid EventId - { - get { return inner.Id; } - } - public long Version { get { return inner.Version; } @@ -55,11 +53,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History get { return inner.Channel; } } - public string EventType - { - get { return inner.Message; } - } - public string Message { get { return message.Value; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs deleted file mode 100644 index 962c632aa..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Entities.Rules.State; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed class MongoRuleEntity : IVersionedEntity - { - [BsonId] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } - - [BsonElement] - [BsonRequired] - [BsonRepresentation(BsonType.String)] - public Guid AppId { get; set; } - - [BsonElement] - [BsonRequired] - [BsonJson] - public RuleState State { get; set; } - - [BsonElement] - [BsonRequired] - public long Version { get; set; } - - [BsonElement] - [BsonRequired] - public bool IsDeleted { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs deleted file mode 100644 index 6ab0b1f3b..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed partial class MongoRuleRepository : MongoRepositoryBase, IRuleRepository - { - public MongoRuleRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "States_Rules"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection) - { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted)); - } - - public async Task> QueryRuleIdsAsync(Guid appId) - { - var ruleEntities = - await Collection.Find(x => x.AppId == appId && !x.IsDeleted).Only(x => x.Id) - .ToListAsync(); - - return ruleEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs deleted file mode 100644 index cd8a2ee02..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Rules.State; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed partial class MongoRuleRepository : ISnapshotStore - { - public async Task<(RuleState Value, long Version)> ReadAsync(Guid key) - { - var existing = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (existing.State, existing.Version); - } - - return (null, EtagVersion.NotFound); - } - - public Task WriteAsync(Guid key, RuleState value, long oldVersion, long newVersion) - { - return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u - .Set(x => x.State, value) - .Set(x => x.AppId, value.AppId.Id) - .Set(x => x.IsDeleted, value.IsDeleted)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs deleted file mode 100644 index 100986972..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Entities.Schemas.State; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas -{ - public sealed class MongoSchemaEntity : IVersionedEntity - { - [BsonId] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } - - [BsonElement] - [BsonRequired] - [BsonRepresentation(BsonType.String)] - public Guid AppId { get; set; } - - [BsonElement] - [BsonRequired] - [BsonJson] - public SchemaState State { get; set; } - - [BsonElement] - [BsonRequired] - public string Name { get; set; } - - [BsonElement] - [BsonRequired] - public long Version { get; set; } - - [BsonElement] - [BsonRequired] - public bool IsDeleted { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs deleted file mode 100644 index df56da8b8..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Schemas.Repositories; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas -{ - public sealed partial class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository - { - public MongoSchemaRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "States_Schemas"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection) - { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Ascending(x => x.IsDeleted)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Ascending(x => x.Name).Ascending(x => x.IsDeleted)); - } - - public async Task FindSchemaIdAsync(Guid appId, string name) - { - var schemaEntity = - await Collection.Find(x => x.AppId == appId && x.Name == name && !x.IsDeleted).Only(x => x.Id).SortByDescending(x => x.Version) - .FirstOrDefaultAsync(); - - return schemaEntity != null ? Guid.Parse(schemaEntity["_id"].AsString) : Guid.Empty; - } - - public async Task> QuerySchemaIdsAsync(Guid appId) - { - var schemaEntities = - await Collection.Find(x => x.AppId == appId && !x.IsDeleted).Only(x => x.Id) - .ToListAsync(); - - return schemaEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs deleted file mode 100644 index a23899a7a..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Schemas.State; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas -{ - public sealed partial class MongoSchemaRepository : ISnapshotStore - { - public async Task<(SchemaState Value, long Version)> ReadAsync(Guid key) - { - var existing = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (existing.State, existing.Version); - } - - return (null, EtagVersion.NotFound); - } - - public Task WriteAsync(Guid key, SchemaState value, long oldVersion, long newVersion) - { - return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u - .Set(x => x.State, value) - .Set(x => x.AppId, value.AppId.Id) - .Set(x => x.Name, value.Name) - .Set(x => x.IsDeleted, value.IsDeleted)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index ef9f2c419..f7ada02c3 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -11,11 +11,8 @@ using System.Linq; using System.Threading.Tasks; using Orleans; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Log; @@ -26,29 +23,15 @@ namespace Squidex.Domain.Apps.Entities public sealed class AppProvider : IAppProvider { private readonly IGrainFactory grainFactory; - private readonly IAppRepository appRepository; - private readonly IRuleRepository ruleRepository; private readonly IRequestCache requestCache; - private readonly ISchemaRepository schemaRepository; - - public AppProvider( - IGrainFactory grainFactory, - IAppRepository appRepository, - ISchemaRepository schemaRepository, - IRuleRepository ruleRepository, - IRequestCache requestCache) + + public AppProvider(IGrainFactory grainFactory, IRequestCache requestCache) { Guard.NotNull(grainFactory, nameof(grainFactory)); - Guard.NotNull(appRepository, nameof(appRepository)); - Guard.NotNull(schemaRepository, nameof(schemaRepository)); Guard.NotNull(requestCache, nameof(requestCache)); - Guard.NotNull(ruleRepository, nameof(ruleRepository)); this.grainFactory = grainFactory; - this.appRepository = appRepository; - this.schemaRepository = schemaRepository; this.requestCache = requestCache; - this.ruleRepository = ruleRepository; } public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) @@ -143,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - var ids = await schemaRepository.QuerySchemaIdsAsync(appId); + var ids = await grainFactory.GetGrain(appId).GetSchemaIdsAsync(); var schemas = await Task.WhenAll( @@ -160,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - var ids = await ruleRepository.QueryRuleIdsAsync(appId); + var ids = await grainFactory.GetGrain(appId).GetRuleIdsAsync(); var rules = await Task.WhenAll( @@ -177,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - var ids = await appRepository.QueryUserAppIdsAsync(userId); + var ids = await grainFactory.GetGrain(userId).GetAppIdsAsync(); var apps = await Task.WhenAll( @@ -192,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - return await appRepository.FindAppIdByNameAsync(name); + return await grainFactory.GetGrain(SingleGrain.Id).GetAppIdAsync(name); } } @@ -200,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - return await schemaRepository.FindSchemaIdAsync(appId, name); + return await grainFactory.GetGrain(appId).GetSchemaIdAsync(name); } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs index e6303003f..370ad85de 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -341,7 +341,7 @@ namespace Squidex.Domain.Apps.Entities.Apps public Task> GetStateAsync() { - return Task.FromResult(new J(Snapshot)); + return J.AsTask(Snapshot); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index cc888be2d..d36f551f0 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -20,43 +20,43 @@ namespace Squidex.Domain.Apps.Entities.Apps : base(typeNameRegistry) { AddEventMessage( - "assigned {user:[Contributor]} as [Permission]."); + "assigned {user:[Contributor]} as {[Permission]}"); AddEventMessage( - "removed {user:[Contributor]}."); + "removed {user:[Contributor]} from app"); AddEventMessage( - "added client {[Id]}."); + "added client {[Id]} to app"); AddEventMessage( - "revoked client {[Id]}."); + "revoked client {[Id]}"); AddEventMessage( - "updated client {[Id]}."); + "updated client {[Id]}"); AddEventMessage( - "renamed client {[Id]} to {[Name]}."); + "renamed client {[Id]} to {[Name]}"); AddEventMessage( - "added language {[Language]}."); + "added language {[Language]}"); AddEventMessage( - "removed language {[Language]}."); + "removed language {[Language]}"); AddEventMessage( - "updated language {[Language]}."); + "updated language {[Language]}"); AddEventMessage( - "changed master language to {[Language]}."); + "changed master language to {[Language]}"); AddEventMessage( - "added pattern {[Pattern]}."); + "added pattern {[Name]}"); AddEventMessage( - "deleted pattern {[Pattern]}."); + "deleted pattern {[Name]}"); AddEventMessage( - "updated pattern {[Pattern]}."); + "updated pattern {[Name]}"); } protected Task On(AppContributorRemoved @event) @@ -146,25 +146,25 @@ namespace Squidex.Domain.Apps.Entities.Apps return Task.FromResult( ForEvent(@event, channel) - .AddParameter("Pattern", @event.Name)); + .AddParameter("Name", @event.Name)); } - protected Task On(AppPatternDeleted @event) + protected Task On(AppPatternUpdated @event) { const string channel = "settings.patterns"; return Task.FromResult( ForEvent(@event, channel) - .AddParameter("Pattern", @event.Name)); + .AddParameter("Name", @event.Name)); } - protected Task On(AppPatternUpdated @event) + protected Task On(AppPatternDeleted @event) { const string channel = "settings.patterns"; return Task.FromResult( ForEvent(@event, channel) - .AddParameter("Pattern", @event.Name)); + .AddParameter("Name", @event.Name)); } protected override Task CreateEventCoreAsync(Envelope @event) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs new file mode 100644 index 000000000..798fd6f1b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByNameIndexCommandMiddleware : ICommandMiddleware + { + private readonly IAppsByNameIndex index; + + public AppsByNameIndexCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + index = grainFactory.GetGrain(SingleGrain.Id); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateApp createApp: + await index.AddAppAsync(createApp.AppId, createApp.Name); + break; + case ArchiveApp archiveApp: + await index.RemoveAppAsync(archiveApp.AppId); + break; + } + } + + await next(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs new file mode 100644 index 000000000..db53e1dc3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByNameIndexGrain : GrainOfString, IAppsByNameIndex + { + private readonly IStore store; + private IPersistence persistence; + private State state = new State(); + + [CollectionName("Index_AppsByName")] + public sealed class State + { + public Dictionary Apps { get; set; } = new Dictionary(); + } + + public AppsByNameIndexGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public override Task OnActivateAsync(string key) + { + persistence = store.WithSnapshots(key, s => + { + state = s; + }); + + return persistence.ReadAsync(); + } + + public Task RebuildAsync(Dictionary apps) + { + state = new State { Apps = apps }; + + return persistence.WriteSnapshotAsync(state); + } + + public Task AddAppAsync(Guid appId, string name) + { + state.Apps[name] = appId; + + return persistence.WriteSnapshotAsync(state); + } + + public Task RemoveAppAsync(Guid appId) + { + state.Apps.Remove(state.Apps.FirstOrDefault(x => x.Value == appId).Key ?? string.Empty); + + return persistence.WriteSnapshotAsync(state); + } + + public Task GetAppIdAsync(string appName) + { + state.Apps.TryGetValue(appName, out var appId); + + return Task.FromResult(appId); + } + + public Task> GetAppIdsAsync() + { + return Task.FromResult(state.Apps.Values.ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs new file mode 100644 index 000000000..3327fc914 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByUserIndexCommandMiddleware : ICommandMiddleware + { + private readonly IGrainFactory grainFactory; + + public AppsByUserIndexCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateApp createApp: + await Index(GetUserId(createApp)).AddAppAsync(createApp.AppId); + break; + case AssignContributor assignContributor: + await Index(GetUserId(context)).AddAppAsync(assignContributor.AppId); + break; + case RemoveContributor removeContributor: + await Index(GetUserId(removeContributor)).RemoveAppAsync(removeContributor.AppId); + break; + case ArchiveApp archiveApp: + { + var appState = await grainFactory.GetGrain(archiveApp.AppId).GetStateAsync(); + + foreach (var contributorId in appState.Value.Contributors.Keys) + { + await Index(contributorId).RemoveAppAsync(archiveApp.AppId); + } + + break; + } + } + } + + await next(); + } + + private static string GetUserId(RemoveContributor removeContributor) + { + return removeContributor.ContributorId; + } + + private static string GetUserId(CreateApp createApp) + { + return createApp.Actor.Identifier; + } + + private static string GetUserId(CommandContext context) + { + return context.Result>().IdOrValue; + } + + private IAppsByUserIndex Index(string id) + { + return grainFactory.GetGrain(id); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs new file mode 100644 index 000000000..f8275bdc7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByUserIndexGrain : GrainOfString, IAppsByUserIndex + { + private readonly IStore store; + private IPersistence persistence; + private State state = new State(); + + [CollectionName("Index_AppsByUser")] + public sealed class State + { + public HashSet Apps { get; set; } = new HashSet(); + } + + public AppsByUserIndexGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public override Task OnActivateAsync(string key) + { + persistence = store.WithSnapshots(key, s => + { + state = s; + }); + + return persistence.ReadAsync(); + } + + public Task RebuildAsync(HashSet apps) + { + state = new State { Apps = apps }; + + return persistence.WriteSnapshotAsync(state); + } + + public Task AddAppAsync(Guid appId) + { + state.Apps.Add(appId); + + return persistence.WriteSnapshotAsync(state); + } + + public Task RemoveAppAsync(Guid appId) + { + state.Apps.Remove(appId); + + return persistence.WriteSnapshotAsync(state); + } + + public Task> GetAppIdsAsync() + { + return Task.FromResult(state.Apps.ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs similarity index 50% rename from src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs rename to src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs index 41cbfde78..9ba30cd1a 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs @@ -1,18 +1,27 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using System.Threading.Tasks; +using Orleans; -namespace Squidex.Domain.Apps.Entities.Rules.Repositories +namespace Squidex.Domain.Apps.Entities.Apps { - public interface IRuleRepository + public interface IAppsByNameIndex : IGrainWithStringKey { - Task> QueryRuleIdsAsync(Guid appId); + Task AddAppAsync(Guid appId, string name); + + Task RemoveAppAsync(Guid appId); + + Task RebuildAsync(Dictionary apps); + + Task GetAppIdAsync(string name); + + Task> GetAppIdsAsync(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs similarity index 55% rename from src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs rename to src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs index c8f1a339e..8b960b13d 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs @@ -1,22 +1,25 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using System.Threading.Tasks; +using Orleans; -namespace Squidex.Domain.Apps.Entities.Apps.Repositories +namespace Squidex.Domain.Apps.Entities.Apps { - public interface IAppRepository + public interface IAppsByUserIndex : IGrainWithStringKey { - Task FindAppIdByNameAsync(string name); + Task AddAppAsync(Guid appId); - Task> QueryAppIdsAsync(); + Task RemoveAppAsync(Guid appId); - Task> QueryUserAppIdsAsync(string userId); + Task RebuildAsync(HashSet apps); + + Task> GetAppIdsAsync(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 4662de274..9a60918c9 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -12,11 +12,12 @@ using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Apps.State { - public class AppState : DomainObjectState, - IAppEntity + [CollectionName("Apps")] + public class AppState : DomainObjectState, IAppEntity { [JsonProperty] public string Name { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index ba0e1a19f..d9d1520b4 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public Task> GetStateAsync() { - return Task.FromResult(new J(Snapshot)); + return J.AsTask(Snapshot); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs index 3ed7714a7..d1cf6df53 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -17,9 +17,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets.State { - public class AssetState : DomainObjectState, - IAssetEntity, - IAssetInfo + public class AssetState : DomainObjectState, IAssetEntity, IAssetInfo { [JsonProperty] public NamedId AppId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs index a612e945c..f1d0a8882 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs @@ -275,7 +275,7 @@ namespace Squidex.Domain.Apps.Entities.Backup public Task>> GetStateAsync() { - return Task.FromResult(new J>(state.Jobs.OfType().ToList())); + return J.AsTask(state.Jobs.OfType().ToList()); } private bool IsRunning() diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 17f91adf8..e218b6c44 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -18,8 +18,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Contents.State { - public class ContentState : DomainObjectState, - IContentEntity + public class ContentState : DomainObjectState, IContentEntity { [JsonProperty] public NamedId AppId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs similarity index 54% rename from src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs rename to src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs index 53294cef3..dd169a045 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs @@ -1,20 +1,25 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using System.Threading.Tasks; +using Orleans; -namespace Squidex.Domain.Apps.Entities.Schemas.Repositories +namespace Squidex.Domain.Apps.Entities.Rules { - public interface ISchemaRepository + public interface IRulesByAppIndex : IGrainWithGuidKey { - Task FindSchemaIdAsync(Guid appId, string name); + Task AddRuleAsync(Guid ruleId); - Task> QuerySchemaIdsAsync(Guid appId); + Task RemoveRuleAsync(Guid ruleId); + + Task RebuildAsync(HashSet rules); + + Task> GetRuleIdsAsync(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs new file mode 100644 index 000000000..faee7b695 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public sealed class RulesByAppIndexCommandMiddleware : ICommandMiddleware + { + private readonly IGrainFactory grainFactory; + + public RulesByAppIndexCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateRule createRule: + await Index(createRule.AppId.Id).AddRuleAsync(createRule.RuleId); + break; + case DeleteRule deleteRule: + { + var schema = await grainFactory.GetGrain(deleteRule.RuleId).GetStateAsync(); + + await Index(schema.Value.AppId.Id).RemoveRuleAsync(deleteRule.RuleId); + + break; + } + } + } + + await next(); + } + + private IRulesByAppIndex Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs new file mode 100644 index 000000000..6f9a49390 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public sealed class RulesByAppIndexGrain : GrainOfGuid, IRulesByAppIndex + { + private readonly IStore store; + private IPersistence persistence; + private State state = new State(); + + [CollectionName("Index_RulesByApp")] + public sealed class State + { + public HashSet Rules { get; set; } = new HashSet(); + } + + public RulesByAppIndexGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public override Task OnActivateAsync(Guid key) + { + persistence = store.WithSnapshots(key, s => + { + state = s; + }); + + return persistence.ReadAsync(); + } + + public Task RebuildAsync(HashSet rules) + { + state = new State { Rules = rules }; + + return persistence.WriteSnapshotAsync(state); + } + + public Task AddRuleAsync(Guid ruleId) + { + state.Rules.Add(ruleId); + + return persistence.WriteSnapshotAsync(state); + } + + public Task RemoveRuleAsync(Guid ruleId) + { + state.Rules.Remove(ruleId); + + return persistence.WriteSnapshotAsync(state); + } + + public Task> GetRuleIdsAsync() + { + return Task.FromResult(state.Rules.ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index c962cf109..f279552e6 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Rules foreach (var ruleEntity in rules) { - var job = ruleService.CreateJob(ruleEntity.RuleDef, @event); + var job = await ruleService.CreateJobAsync(ruleEntity.RuleDef, @event); if (job != null) { diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs index bea4bc7b5..81e475e4d 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs @@ -130,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Rules public Task> GetStateAsync() { - return Task.FromResult(new J(Snapshot)); + return J.AsTask(Snapshot); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs index fa87078da..80da5ed16 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs @@ -13,11 +13,12 @@ using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Rules.State { - public class RuleState : DomainObjectState, - IRuleEntity + [CollectionName("Rules")] + public class RuleState : DomainObjectState, IRuleEntity { [JsonProperty] public NamedId AppId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs new file mode 100644 index 000000000..38104d363 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public interface ISchemasByAppIndex : IGrainWithGuidKey + { + Task AddSchemaAsync(Guid schemaId, string name); + + Task RemoveSchemaAsync(Guid schemaId); + + Task RebuildAsync(Dictionary schemas); + + Task GetSchemaIdAsync(string name); + + Task> GetSchemaIdsAsync(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs new file mode 100644 index 000000000..1090894d5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public sealed class SchemasByAppIndexCommandMiddleware : ICommandMiddleware + { + private readonly IGrainFactory grainFactory; + + public SchemasByAppIndexCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateSchema createSchema: + await Index(createSchema.AppId.Id).AddSchemaAsync(createSchema.SchemaId, createSchema.Name); + break; + case DeleteSchema deleteSchema: + { + var schema = await grainFactory.GetGrain(deleteSchema.SchemaId).GetStateAsync(); + + await Index(schema.Value.AppId.Id).RemoveSchemaAsync(deleteSchema.SchemaId); + + break; + } + } + } + + await next(); + } + + private ISchemasByAppIndex Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs new file mode 100644 index 000000000..9164936c9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public sealed class SchemasByAppIndexGrain : GrainOfGuid, ISchemasByAppIndex + { + private readonly IStore store; + private IPersistence persistence; + private State state = new State(); + + [CollectionName("Index_SchemasByApp")] + public sealed class State + { + public Dictionary Schemas { get; set; } = new Dictionary(); + } + + public SchemasByAppIndexGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public override Task OnActivateAsync(Guid key) + { + persistence = store.WithSnapshots(key, s => + { + state = s; + }); + + return persistence.ReadAsync(); + } + + public Task RebuildAsync(Dictionary schemas) + { + state = new State { Schemas = schemas }; + + return persistence.WriteSnapshotAsync(state); + } + + public Task AddSchemaAsync(Guid schemaId, string name) + { + state.Schemas[name] = schemaId; + + return persistence.WriteSnapshotAsync(state); + } + + public Task RemoveSchemaAsync(Guid schemaId) + { + state.Schemas.Remove(state.Schemas.FirstOrDefault(x => x.Value == schemaId).Key ?? string.Empty); + + return persistence.WriteSnapshotAsync(state); + } + + public Task GetSchemaIdAsync(string name) + { + state.Schemas.TryGetValue(name, out var schemaId); + + return Task.FromResult(schemaId); + } + + public Task> GetSchemaIdsAsync() + { + return Task.FromResult(state.Schemas.Values.ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index 5e61d86ba..95a1c7b02 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -305,7 +305,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas public Task> GetStateAsync() { - return Task.FromResult(new J(Snapshot)); + return J.AsTask(Snapshot); } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index bc148430b..5f09910e6 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -15,11 +15,12 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Schemas.State { - public class SchemaState : DomainObjectState, - ISchemaEntity + [CollectionName("Schemas")] + public class SchemaState : DomainObjectState, ISchemaEntity { [JsonProperty] public NamedId AppId { get; set; } diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 19f641f38..873f49a1c 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -5,7 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Linq; using System.Threading.Tasks; +using MongoDB.Bson; using MongoDB.Driver; using Newtonsoft.Json; using Squidex.Infrastructure.MongoDb; @@ -26,7 +28,11 @@ namespace Squidex.Infrastructure.States protected override string CollectionName() { - return $"States_{typeof(T).Name}"; + var attribute = typeof(T).GetCustomAttributes(true).OfType().FirstOrDefault(); + + var name = attribute?.Name ?? typeof(T).Name; + + return $"States_{name}"; } public async Task<(T Value, long Version)> ReadAsync(TKey key) @@ -47,5 +53,10 @@ namespace Squidex.Infrastructure.States { return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.Doc, value)); } + + public Task ReadAllAsync(System.Func callback) + { + return Collection.Find(new BsonDocument()).ForEachAsync(x => callback(x.Doc, x.Version)); + } } } diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs index f85bf59ab..bbea29936 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs @@ -11,6 +11,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { + [BsonIgnoreExtraElements] public sealed class MongoState : IVersionedEntity { [BsonId] diff --git a/src/Squidex.Infrastructure/CachingProviderBase.cs b/src/Squidex.Infrastructure/CachingProviderBase.cs index c96cdfdbe..d52e3be7a 100644 --- a/src/Squidex.Infrastructure/CachingProviderBase.cs +++ b/src/Squidex.Infrastructure/CachingProviderBase.cs @@ -6,7 +6,6 @@ // ========================================================================== using Microsoft.Extensions.Caching.Memory; -using Squidex.Infrastructure; namespace Squidex.Infrastructure { diff --git a/src/Squidex.Infrastructure/Commands/CommandContext.cs b/src/Squidex.Infrastructure/Commands/CommandContext.cs index a83ba04fb..49fee10e8 100644 --- a/src/Squidex.Infrastructure/Commands/CommandContext.cs +++ b/src/Squidex.Infrastructure/Commands/CommandContext.cs @@ -45,9 +45,11 @@ namespace Squidex.Infrastructure.Commands this.commandBus = commandBus; } - public void Complete(object resultValue = null) + public CommandContext Complete(object resultValue = null) { result = Tuple.Create(resultValue); + + return this; } public T Result() diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs index adbca58e7..6e3da7063 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs @@ -7,6 +7,7 @@ using System; using Orleans; +using Squidex.Infrastructure.Orleans; namespace Squidex.Infrastructure.EventSourcing.Grains { @@ -20,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains eventConsumerManagerGrain = new Lazy(() => { - return factory.GetGrain("Default"); + return factory.GetGrain(SingleGrain.Id); }); } diff --git a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs index 254d31b49..5a3bb7a19 100644 --- a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs +++ b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs @@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.Orleans { try { - var grain = grainFactory.GetGrain("Default"); + var grain = grainFactory.GetGrain(SingleGrain.Id); await grain.ActivateAsync(); diff --git a/src/Squidex.Infrastructure/Orleans/J.cs b/src/Squidex.Infrastructure/Orleans/J.cs index cd1190790..2f482d67b 100644 --- a/src/Squidex.Infrastructure/Orleans/J.cs +++ b/src/Squidex.Infrastructure/Orleans/J.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading.Tasks; using Newtonsoft.Json; #pragma warning disable SA1401 // Fields must be private @@ -14,5 +15,15 @@ namespace Squidex.Infrastructure.Orleans public static class J { public static JsonSerializer Serializer = new JsonSerializer(); + + public static J Of(T value) + { + return value; + } + + public static Task> AsTask(T value) + { + return Task.FromResult>(value); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs b/src/Squidex.Infrastructure/Orleans/SingleGrain.cs similarity index 60% rename from src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs rename to src/Squidex.Infrastructure/Orleans/SingleGrain.cs index 59fb244d7..66a9fb356 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs +++ b/src/Squidex.Infrastructure/Orleans/SingleGrain.cs @@ -1,16 +1,14 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; -using Newtonsoft.Json.Linq; - -namespace Squidex.Domain.Apps.Core.Rules +namespace Squidex.Infrastructure.Orleans { - public sealed class RuleJobData : Dictionary + public static class SingleGrain { + public const string Id = "Default"; } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs b/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs similarity index 57% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs rename to src/Squidex.Infrastructure/States/CollectionNameAttribute.cs index 0cd2e4257..647420a4b 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs +++ b/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs @@ -5,18 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; +using System; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Infrastructure.States { - public interface IContentScheduleItem + [AttributeUsage(AttributeTargets.Class)] + public sealed class CollectionNameAttribute : Attribute { - Status ScheduledTo { get; } + public string Name { get; } - Instant ScheduledAt { get; } - - RefToken ScheduledBy { get; } + public CollectionNameAttribute(string name) + { + Name = name; + } } } diff --git a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs b/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs index 500a44dbf..b2c00d6b7 100644 --- a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs +++ b/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs @@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.States { public sealed class DefaultStreamNameResolver : IStreamNameResolver { - private static readonly string[] Suffixes = { "Grain", "DomainObject" }; + private static readonly string[] Suffixes = { "Grain", "DomainObject", "State" }; public string GetStreamName(Type aggregateType, string id) { diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/src/Squidex.Infrastructure/States/ISnapshotStore.cs index d20fc7437..60b2ab5be 100644 --- a/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading.Tasks; namespace Squidex.Infrastructure.States @@ -16,5 +17,7 @@ namespace Squidex.Infrastructure.States Task<(T Value, long Version)> ReadAsync(TKey key); Task ClearAsync(); + + Task ReadAllAsync(Func callback); } } diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs index 8265d5835..fd5abdeae 100644 --- a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs +++ b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs @@ -13,6 +13,7 @@ using Orleans; using Squidex.Areas.Api.Controllers.EventConsumers.Models; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Orleans; using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers.EventConsumers @@ -28,7 +29,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers public EventConsumersController(ICommandBus commandBus, IGrainFactory grainFactory) : base(commandBus) { - eventConsumerManagerGrain = grainFactory.GetGrain("Default"); + eventConsumerManagerGrain = grainFactory.GetGrain(SingleGrain.Id); } [HttpGet] diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs index afd3e8094..f250e1352 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -46,6 +46,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public bool IsListField { get; set; } + /// + /// Optional url to the editor. + /// + public string EditorUrl { get; set; } + /// /// Gets the partitioning of the language, e.g. invariant or language. /// diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs index 317f69794..8dc17e028 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; -using Squidex.Domain.Users; using Squidex.Infrastructure.Reflection; using Squidex.Shared.Users; diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 689991790..854457911 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Backup; @@ -28,7 +29,9 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; @@ -116,25 +119,22 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - - services.AddSingleton>(DomainObjectGrainFormatter.Format); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingleton>(DomainObjectGrainFormatter.Format); services.AddSingleton(c => { @@ -172,7 +172,10 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); - services.AddTransientAs() + services.AddTransientAs() + .As(); + + services.AddTransientAs() .As(); services.AddTransientAs() diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index f4f8e56ba..3e6909af1 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -11,34 +11,24 @@ using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Migrate_01.Migrations; using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps.Repositories; -using Squidex.Domain.Apps.Entities.Apps.State; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Entities.Backup.State; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History.Repositories; -using Squidex.Domain.Apps.Entities.MongoDb.Apps; using Squidex.Domain.Apps.Entities.MongoDb.Assets; using Squidex.Domain.Apps.Entities.MongoDb.Contents; using Squidex.Domain.Apps.Entities.MongoDb.History; using Squidex.Domain.Apps.Entities.MongoDb.Rules; -using Squidex.Domain.Apps.Entities.MongoDb.Schemas; using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Domain.Apps.Entities.Rules.State; -using Squidex.Domain.Apps.Entities.Schemas.Repositories; -using Squidex.Domain.Apps.Entities.Schemas.State; using Squidex.Domain.Users; using Squidex.Domain.Users.MongoDb; using Squidex.Domain.Users.MongoDb.Infrastructure; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; @@ -62,63 +52,55 @@ namespace Squidex.Config.Domain var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); var mongoContentDatabase = mongoClient.GetDatabase(mongoContentDatabaseName); - services.AddSingletonAs(c => new MongoXmlRepository(mongoDatabase)) - .As() - .As(); + services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>)); - services.AddSingletonAs(c => new MongoMigrationStatus(mongoDatabase)) - .As() - .As(); + services.AddSingletonAs(mongoDatabase) + .As(); - services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) - .As>() + services.AddSingletonAs() + .As() .As(); - services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) - .As>() + services.AddSingletonAs() + .As() .As(); - services.AddSingletonAs(c => new MongoPersistedGrantStore(mongoDatabase)) + services.AddSingletonAs() .As() .As(); - services.AddSingletonAs(c => new MongoUsageStore(mongoDatabase)) + services.AddSingletonAs() .As() .As(); - services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase)) + services.AddSingletonAs() .As() .As(); - services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) + services.AddSingletonAs() .As>() .As() .As() .As(); - services.AddSingletonAs(c => new MongoRoleStore(mongoDatabase)) + services.AddSingletonAs() .As>() .As() .As(); - services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase)) - .As() - .As>() - .As(); - - services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) - .As() - .As>() + services.AddSingletonAs() + .As() + .As() .As(); - services.AddSingletonAs(c => new MongoRuleRepository(mongoDatabase)) - .As() - .As>() + services.AddSingletonAs() + .As() + .As() .As(); - services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase)) - .As() - .As>() + services.AddSingletonAs() + .As() + .As>() .As(); services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) @@ -127,15 +109,11 @@ namespace Squidex.Config.Domain .As() .As(); - services.AddSingletonAs(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices())) - .As() - .As() - .As(); + services.AddTransientAs(c => new DeleteArchiveCollectionSetup(mongoContentDatabase)) + .As(); - services.AddSingletonAs(c => new MongoAssetStatsRepository(mongoDatabase)) - .As() - .As() - .As(); + services.AddTransientAs() + .As(); } }); diff --git a/src/Squidex/Config/Orleans/SiloWrapper.cs b/src/Squidex/Config/Orleans/SiloWrapper.cs index dd9593d99..647dab956 100644 --- a/src/Squidex/Config/Orleans/SiloWrapper.cs +++ b/src/Squidex/Config/Orleans/SiloWrapper.cs @@ -7,7 +7,6 @@ using System; using System.Diagnostics; -using System.Net; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -57,7 +56,7 @@ namespace Squidex.Config.Orleans J.Serializer = SerializationServices.DefaultJsonSerializer; var hostBuilder = new SiloHostBuilder() - .UseDashboard(options => options.HostSelf = true) + .UseDashboard(options => options.HostSelf = false) .AddStartupTask>() .AddStartupTask>() .AddStartupTask>() diff --git a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs index 2e4b36aa2..9f92da4da 100644 --- a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs +++ b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs @@ -79,7 +79,7 @@ namespace Squidex.Pipeline.Swagger public static SwaggerSecurityScheme CreateOAuthSchema(MyUrlsOptions urlOptions) { - var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token"); + var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false); var securityDocs = LoadDocs("security"); var securityText = securityDocs.Replace("", tokenUrl); diff --git a/src/Squidex/Program.cs b/src/Squidex/Program.cs index f6b653bde..48812e334 100644 --- a/src/Squidex/Program.cs +++ b/src/Squidex/Program.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 672986d8a..12eafe95b 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -21,99 +21,107 @@ -
-
-
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- - -
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/src/Squidex/app/features/content/shared/assets-editor.component.ts index 069a53e67..155beccb4 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.ts +++ b/src/Squidex/app/features/content/shared/assets-editor.component.ts @@ -48,20 +48,24 @@ export class AssetsEditorComponent implements ControlValueAccessor { ) { } - public writeValue(value: string[]) { - if (Types.isArrayOfString(value) && !Types.isEquals(value, this.oldAssets.map(x => x.id).values)) { - const assetIds: string[] = value; - - this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, value) - .subscribe(dtos => { - this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!)); - - if (this.oldAssets.length !== assetIds.length) { - this.updateValue(); - } - }, () => { - this.oldAssets = ImmutableArray.empty(); - }); + public writeValue(obj: any) { + if (Types.isArrayOfString(obj)) { + if (!Types.isEquals(obj, this.oldAssets.map(x => x.id).values)) { + const assetIds: string[] = obj; + + this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, obj) + .subscribe(dtos => { + this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!)); + + if (this.oldAssets.length !== assetIds.length) { + this.updateValue(); + } + }, () => { + this.oldAssets = ImmutableArray.empty(); + }); + } + } else { + this.oldAssets = ImmutableArray.empty(); } } diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index 7a5f50c04..fe060b63a 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -27,7 +27,7 @@
- +
+ + + Url to your plugin if you use a custom editor. + +
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts index 6fa536fdf..130698031 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts @@ -31,11 +31,6 @@ export class BooleanUIComponent implements OnInit { Validators.required ])); - this.editForm.setControl('placeholder', - new FormControl(this.properties.placeholder, [ - Validators.maxLength(100) - ])); - this.editForm.setControl('inlineEditable', new FormControl(this.properties.inlineEditable)); } diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html index b888ce094..7802e154a 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html @@ -1,4 +1,15 @@
+
+ + +
+ + + + Url to your plugin if you use a custom editor. + +
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts index eabd30c6e..b145acbed 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts @@ -35,10 +35,5 @@ export class DateTimeUIComponent implements OnInit { new FormControl(this.properties.editor, [ Validators.required ])); - - this.editForm.setControl('placeholder', - new FormControl(this.properties.placeholder, [ - Validators.maxLength(100) - ])); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html index 6bfee94c9..d5dd76e57 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html @@ -1,4 +1,15 @@
+
+ + +
+ + + + Url to your plugin if you use a custom editor. + +
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html index 9a0e7afd1..c41e111c6 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html @@ -1,3 +1,13 @@
- Nothing to setup +
+ + +
+ + + + Url to your plugin if you use a custom editor. + +
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss b/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss index fbb752506..1f393a181 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss +++ b/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss @@ -1,2 +1,6 @@ @import '_vars'; -@import '_mixins'; \ No newline at end of file +@import '_mixins'; + +.form-group { + margin: 0; +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html index 357cb7826..75c821ab4 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html @@ -1,4 +1,15 @@
+
+ + +
+ + + + Url to your plugin if you use a custom editor. + +
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts index 0c34229b1..94b6a6e7d 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts @@ -45,11 +45,6 @@ export class NumberUIComponent implements OnDestroy, OnInit { Validators.required ])); - this.editForm.setControl('placeholder', - new FormControl(this.properties.placeholder, [ - Validators.maxLength(100) - ])); - this.editForm.setControl('allowedValues', new FormControl(this.properties.allowedValues, [])); diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html index d25ff9340..eebeb9636 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html @@ -1,4 +1,15 @@
+
+ + +
+ + + + Url to your plugin if you use a custom editor. + +
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts index 7907eff8c..06991f00d 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts @@ -43,11 +43,6 @@ export class StringUIComponent implements OnDestroy, OnInit { Validators.required ])); - this.editForm.setControl('placeholder', - new FormControl(this.properties.placeholder, [ - Validators.maxLength(100) - ])); - this.editForm.setControl('allowedValues', new FormControl(this.properties.allowedValues)); diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html index c7b95a2ea..a1f03b011 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html @@ -17,7 +17,7 @@ - + The schema name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appsState.appName}}/{{createForm.schemaName | async}}/. diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html index d33e5fd2a..ab463d7dd 100644 --- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html +++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html @@ -27,7 +27,7 @@
- +
diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts index 4b2303a91..6e80b4379 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts @@ -15,6 +15,7 @@ import { AssignContributorForm, AutocompleteSource, ContributorsState, + Types, UserDto, UsersService } from '@app/shared'; @@ -85,7 +86,7 @@ export class ContributorsPageComponent implements OnInit { if (value) { let user = value.user; - if (user instanceof UserDto) { + if (Types.is(user, UserDto)) { user = user.id; } diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts index 8b4b74aca..4c3710c68 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts @@ -104,14 +104,14 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O return true; } - public writeValue(value: any) { - if (!value) { + public writeValue(obj: any) { + if (!obj) { this.resetForm(); } else { - const item = this.items.find(i => i === value); + const item = this.items.find(i => i === obj); if (item) { - this.queryInput.setValue(value.title || ''); + this.queryInput.setValue(obj.title || ''); } } diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/src/Squidex/app/framework/angular/forms/control-errors.component.ts index b14a04c73..c6c2cb653 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.ts @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnC import { AbstractControl, FormGroupDirective } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; -import { fadeAnimation } from '@app/framework/internal'; +import { fadeAnimation, Types } from '@app/framework/internal'; const DEFAULT_ERRORS: { [key: string]: string } = { required: '{field} is required.', @@ -71,7 +71,7 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { if (this.fieldName) { this.displayFieldName = this.fieldName; } else if (this.for) { - if (typeof this.for === 'string') { + if (Types.isString(this.for)) { this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1); } else { this.displayFieldName = 'field'; @@ -80,7 +80,7 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { let control: AbstractControl | null = null; - if (typeof this.for === 'string') { + if (Types.isString(this.for)) { control = this.formGroupDirective.form.controls[this.for]; } else { control = this.for; diff --git a/src/Squidex/app/framework/angular/forms/copy.directive.ts b/src/Squidex/app/framework/angular/forms/copy.directive.ts index 1f6d593b7..a46d78164 100644 --- a/src/Squidex/app/framework/angular/forms/copy.directive.ts +++ b/src/Squidex/app/framework/angular/forms/copy.directive.ts @@ -40,7 +40,7 @@ export class CopyDirective { element.focus(); - if (element instanceof HTMLInputElement) { + if (Types.is(element, HTMLInputElement)) { element.setSelectionRange(0, element.value.length); } @@ -56,7 +56,7 @@ export class CopyDirective { currentFocus.focus(); } - if (element instanceof HTMLInputElement) { + if (Types.is(element, HTMLInputElement)) { element.setSelectionRange(prevSelectionStart!, prevSelectionEnd!); } } diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts index 0d8c02c3f..6dbc72276 100644 --- a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts @@ -89,18 +89,18 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, }); } - public writeValue(value: string) { - if (!Types.isString(value) || value.length === 0) { - this.timeValue = null; - this.dateValue = null; - } else { - const parsed = moment.parseZone(value); + public writeValue(obj: any) { + if (Types.isString(obj) && obj.length > 0) { + const parsed = moment.parseZone(obj); this.dateValue = moment(parsed); if (this.showTime) { this.timeValue = moment(parsed); } + } else { + this.timeValue = null; + this.dateValue = null; } this.updateControls(); diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.ts b/src/Squidex/app/framework/angular/forms/dropdown.component.ts index b3af31325..8abf7d748 100644 --- a/src/Squidex/app/framework/angular/forms/dropdown.component.ts +++ b/src/Squidex/app/framework/angular/forms/dropdown.component.ts @@ -59,8 +59,8 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor } } - public writeValue(value: any) { - this.selectIndex(this.items && value ? this.items.indexOf(value) : 0); + public writeValue(obj: any) { + this.selectIndex(this.items && obj ? this.items.indexOf(obj) : 0); } public setDisabledState(isDisabled: boolean): void { diff --git a/src/Squidex/app/framework/angular/forms/file-drop.directive.ts b/src/Squidex/app/framework/angular/forms/file-drop.directive.ts index 52cc95610..0238b1e04 100644 --- a/src/Squidex/app/framework/angular/forms/file-drop.directive.ts +++ b/src/Squidex/app/framework/angular/forms/file-drop.directive.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Directive, ElementRef, EventEmitter, HostListener, Output, Renderer } from '@angular/core'; +import { Directive, ElementRef, EventEmitter, HostListener, Output, Renderer2 } from '@angular/core'; @Directive({ selector: '[sqxFileDrop]' @@ -18,7 +18,7 @@ export class FileDropDirective { constructor( private readonly element: ElementRef, - private readonly renderer: Renderer + private readonly renderer: Renderer2 ) { } @@ -71,7 +71,7 @@ export class FileDropDirective { this.dragCounter++; if (this.dragCounter === 1) { - this.renderer.setElementClass(this.element.nativeElement, 'drag', true); + this.renderer.addClass(this.element.nativeElement, 'drag'); } } @@ -79,7 +79,7 @@ export class FileDropDirective { this.dragCounter = number || this.dragCounter - 1; if (this.dragCounter === 0) { - this.renderer.setElementClass(this.element.nativeElement, 'drag', false); + this.renderer.removeClass(this.element.nativeElement, 'drag'); } } diff --git a/src/Squidex/app/framework/angular/forms/focus-on-init.directive.spec.ts b/src/Squidex/app/framework/angular/forms/focus-on-init.directive.spec.ts index a5a47d69d..ec13649f4 100644 --- a/src/Squidex/app/framework/angular/forms/focus-on-init.directive.spec.ts +++ b/src/Squidex/app/framework/angular/forms/focus-on-init.directive.spec.ts @@ -5,33 +5,51 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ElementRef, Renderer } from '@angular/core'; +import { ElementRef } from '@angular/core'; import { FocusOnInitDirective } from './focus-on-init.directive'; describe('FocusOnInitDirective', () => { + let isFocusCalled = false; + let isSelectCalled = false; + + const element: ElementRef = { + nativeElement: { + focus: () => { + isFocusCalled = true; + }, + select: () => { + isSelectCalled = true; + } + } + }; + + beforeEach(() => { + isFocusCalled = false; + isSelectCalled = false; + }); + it('should call focus on element when init', (cb) => { - const calledMethods: string[] = []; - const calledElements: any[] = []; + const directive = new FocusOnInitDirective(element); + directive.select = false; + directive.ngAfterViewInit(); - const renderer = { - invokeElementMethod: (elem: any, method: any) => { - calledElements.push(elem); - calledMethods.push(method); - } - }; + setTimeout(() => { + expect(isFocusCalled).toBeTruthy(); + expect(isSelectCalled).toBeFalsy(); - const element: ElementRef = { - nativeElement: {} - }; + cb(); + }, 200); + }); - const directive = new FocusOnInitDirective(element, renderer as Renderer); + it('should call select on element when init', (cb) => { + const directive = new FocusOnInitDirective(element); directive.select = true; directive.ngAfterViewInit(); setTimeout(() => { - expect(calledMethods).toEqual(['focus', 'select']); - expect(calledElements).toEqual([element.nativeElement, element.nativeElement]); + expect(isFocusCalled).toBeTruthy(); + expect(isSelectCalled).toBeTruthy(); cb(); }, 200); diff --git a/src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts b/src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts index 1a3cbcc3a..954888fb4 100644 --- a/src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts +++ b/src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts @@ -5,7 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, Input, Renderer } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core'; + +import { Types } from '@app/framework/internal'; @Directive({ selector: '[sqxFocusOnInit]' @@ -15,17 +17,20 @@ export class FocusOnInitDirective implements AfterViewInit { public select: boolean; constructor( - private readonly element: ElementRef, - private readonly renderer: Renderer + private readonly element: ElementRef ) { } public ngAfterViewInit() { setTimeout(() => { - this.renderer.invokeElementMethod(this.element.nativeElement, 'focus', []); + if (Types.isFunction(this.element.nativeElement.focus)) { + this.element.nativeElement.focus(); + } if (this.select) { - this.renderer.invokeElementMethod(this.element.nativeElement, 'select', []); + if (Types.isFunction(this.element.nativeElement.select)) { + this.element.nativeElement.select(); + } } }, 100); } diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.html b/src/Squidex/app/framework/angular/forms/iframe-editor.component.html new file mode 100644 index 000000000..44f811b58 --- /dev/null +++ b/src/Squidex/app/framework/angular/forms/iframe-editor.component.html @@ -0,0 +1 @@ + diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.scss b/src/Squidex/app/framework/angular/forms/iframe-editor.component.scss new file mode 100644 index 000000000..a095b5177 --- /dev/null +++ b/src/Squidex/app/framework/angular/forms/iframe-editor.component.scss @@ -0,0 +1,8 @@ +@import '_mixins'; +@import '_vars'; + +iframe { + border: 0; + background: 0; + overflow: hidden; +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts new file mode 100644 index 000000000..867e00c6d --- /dev/null +++ b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts @@ -0,0 +1,113 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { DomSanitizer } from '@angular/platform-browser'; + +import { Types } from '@app/framework/internal'; + +export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IFrameEditorComponent), multi: true +}; + +@Component({ + selector: 'sqx-iframe-editor', + styleUrls: ['./iframe-editor.component.scss'], + templateUrl: './iframe-editor.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR] +}) +export class IFrameEditorComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { + private windowMessageListener: Function; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; + private value: any; + private isDisabled = false; + private isInitialized = false; + private plugin: HTMLIFrameElement; + + @ViewChild('iframe') + public iframe: ElementRef; + + @Input() + public url: string; + + constructor( + private readonly sanitizer: DomSanitizer, + private readonly renderer: Renderer2 + ) { + } + + public ngOnDestroy() { + this.windowMessageListener(); + } + + public ngAfterViewInit() { + this.plugin = this.iframe.nativeElement; + } + + public ngOnInit(): void { + this.windowMessageListener = + this.renderer.listen('window', 'message', (event: MessageEvent) => { + if (event.source === this.plugin.contentWindow) { + const { type } = event.data; + + if (type === 'started') { + this.isInitialized = true; + + if (this.plugin.contentWindow && Types.isFunction(this.plugin.contentWindow.postMessage)) { + this.plugin.contentWindow.postMessage({ type: 'disabled', disabled: this.isDisabled }, '*'); + this.plugin.contentWindow.postMessage({ type: 'valueChanged', value: this.value }, '*'); + } + } else if (type === 'resize') { + const { height } = event.data; + + this.plugin.height = height + 'px'; + } else if (type === 'valueChanged') { + const { value } = event.data; + + if (!Types.jsJsonEquals(this.value, value)) { + this.value = value; + + this.callChange(value); + } + } else if (type === 'touched') { + this.callTouched(); + } + } + }); + } + + public sanitizedUrl() { + return this.sanitizer.bypassSecurityTrustResourceUrl(this.url); + } + + public writeValue(obj: any) { + this.value = obj; + + if (this.isInitialized && this.plugin.contentWindow && Types.isFunction(this.plugin.contentWindow.postMessage)) { + this.plugin.contentWindow.postMessage({ type: 'valueChanged', value: this.value }, '*'); + } + } + + public setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + + if (this.isInitialized && this.plugin.contentWindow && Types.isFunction(this.plugin.contentWindow.postMessage)) { + this.plugin.contentWindow.postMessage({ type: 'disabled', disabled: this.isDisabled }, '*'); + } + } + + public registerOnChange(fn: any) { + this.callChange = fn; + } + + public registerOnTouched(fn: any) { + this.callTouched = fn; + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/indeterminate-value.directive.ts b/src/Squidex/app/framework/angular/forms/indeterminate-value.directive.ts index bbe7d7d7e..5bcf73c68 100644 --- a/src/Squidex/app/framework/angular/forms/indeterminate-value.directive.ts +++ b/src/Squidex/app/framework/angular/forms/indeterminate-value.directive.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Directive, ElementRef, forwardRef, HostListener, Renderer } from '@angular/core'; +import { Directive, ElementRef, forwardRef, HostListener, Renderer2 } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Types } from '@app/framework/internal'; @@ -23,8 +23,8 @@ export class IndeterminateValueDirective implements ControlValueAccessor { private callTouched = () => { /* NOOP */ }; constructor( - private readonly renderer: Renderer, - private readonly element: ElementRef + private readonly element: ElementRef, + private readonly renderer: Renderer2 ) { } @@ -38,18 +38,18 @@ export class IndeterminateValueDirective implements ControlValueAccessor { this.callTouched(); } - public writeValue(value: boolean | number | undefined) { - if (!Types.isBoolean(value)) { - this.renderer.setElementProperty(this.element.nativeElement, 'indeterminate', true); - this.renderer.setElementProperty(this.element.nativeElement, 'checked', false); + public writeValue(obj: any) { + if (!Types.isBoolean(obj)) { + this.renderer.setProperty(this.element.nativeElement, 'indeterminate', true); + this.renderer.setProperty(this.element.nativeElement, 'checked', false); } else { - this.renderer.setElementProperty(this.element.nativeElement, 'indeterminate', false); - this.renderer.setElementProperty(this.element.nativeElement, 'checked', value); + this.renderer.setProperty(this.element.nativeElement, 'indeterminate', false); + this.renderer.setProperty(this.element.nativeElement, 'checked', obj); } } public setDisabledState(isDisabled: boolean): void { - this.renderer.setElementProperty(this.element.nativeElement, 'disabled', isDisabled); + this.renderer.setProperty(this.element.nativeElement, 'disabled', isDisabled); } public registerOnChange(fn: any) { diff --git a/src/Squidex/app/framework/angular/forms/jscript-editor.component.ts b/src/Squidex/app/framework/angular/forms/jscript-editor.component.ts index 98334d94d..9d8a506fe 100644 --- a/src/Squidex/app/framework/angular/forms/jscript-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/jscript-editor.component.ts @@ -9,7 +9,7 @@ import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@an import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; -import { ResourceLoaderService, Types } from '@app/framework/internal'; +import { ResourceLoaderService } from '@app/framework/internal'; declare var ace: any; @@ -39,8 +39,8 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn ) { } - public writeValue(value: string) { - this.value = Types.isString(value) ? value : ''; + public writeValue(obj: any) { + this.value = obj + ''; if (this.aceEditor) { this.setValue(this.value); diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.ts b/src/Squidex/app/framework/angular/forms/json-editor.component.ts index c6a45f350..0e182da13 100644 --- a/src/Squidex/app/framework/angular/forms/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/json-editor.component.ts @@ -40,17 +40,17 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit ) { } - public writeValue(value: any) { - this.value = value; + public writeValue(obj: any) { + this.value = obj; try { - this.valueString = JSON.stringify(value); + this.valueString = JSON.stringify(obj); } catch (e) { this.valueString = ''; } if (this.aceEditor) { - this.setValue(value); + this.setValue(obj); } } diff --git a/src/Squidex/app/framework/angular/forms/lowercase-input.directive.ts b/src/Squidex/app/framework/angular/forms/lowercase-input.directive.ts deleted file mode 100644 index 982ddc703..000000000 --- a/src/Squidex/app/framework/angular/forms/lowercase-input.directive.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Directive, ElementRef, forwardRef, HostListener, Renderer } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { Types } from '@app/framework/internal'; - -export const SQX_LOWERCASE_INPUT_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => LowerCaseInputDirective), multi: true -}; - -@Directive({ - selector: '[sqxLowerCaseInput]', - providers: [SQX_LOWERCASE_INPUT_VALUE_ACCESSOR] -}) -export class LowerCaseInputDirective implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - - constructor( - private readonly element: ElementRef, - private readonly renderer: Renderer - ) { - } - - @HostListener('input', ['$event.target.value']) - public onChange(value: any) { - const normalizedValue = this.transform(value); - - this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); - this.callChange(normalizedValue); - } - - @HostListener('blur') - public onTouched() { - this.callTouched(); - } - - public writeValue(value: string) { - const normalizedValue = this.transform(value); - - this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); - } - - public setDisabledState(isDisabled: boolean): void { - this.renderer.setElementProperty(this.element.nativeElement, 'disabled', isDisabled); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - - private transform(value: any): string { - return Types.isString(value) ? value.toLowerCase() : ''; - } -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts b/src/Squidex/app/framework/angular/forms/progress-bar.component.ts index ac769399b..31d6f3dee 100644 --- a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts +++ b/src/Squidex/app/framework/angular/forms/progress-bar.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, ElementRef, Input, OnChanges, OnInit, Renderer, SimpleChanges } from '@angular/core'; +import { Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; const ProgressBar = require('progressbar.js'); @@ -36,7 +36,7 @@ export class ProgressBarComponent implements OnChanges, OnInit { constructor( private readonly element: ElementRef, - private readonly renderer: Renderer + private readonly renderer: Renderer2 ) { } @@ -48,7 +48,7 @@ export class ProgressBarComponent implements OnChanges, OnInit { strokeWidth: this.strokeWidth }; - this.renderer.setElementStyle(this.element.nativeElement, 'display', 'block'); + this.renderer.setStyle(this.element.nativeElement, 'display', 'block'); if (this.mode === 'Circle') { this.progressBar = new ProgressBar.Circle(this.element.nativeElement, options); diff --git a/src/Squidex/app/framework/angular/forms/slider.component.ts b/src/Squidex/app/framework/angular/forms/slider.component.ts index 84d363565..7e03256aa 100644 --- a/src/Squidex/app/framework/angular/forms/slider.component.ts +++ b/src/Squidex/app/framework/angular/forms/slider.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, ElementRef, forwardRef, Input, Renderer, ViewChild } from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Types } from '@app/framework/internal'; @@ -47,10 +47,10 @@ export class SliderComponent implements ControlValueAccessor { @Input() public step = 1; - constructor(private readonly renderer: Renderer) { } + constructor(private readonly renderer: Renderer2) { } - public writeValue(value: number) { - this.lastValue = this.value = Types.isNumber(value) ? value : 0; + public writeValue(obj: any) { + this.lastValue = this.value = Types.isNumber(obj) ? obj : 0; this.updateThumbPosition(); } @@ -87,16 +87,16 @@ export class SliderComponent implements ControlValueAccessor { this.centerStartOffset = event.offsetX - this.thumb.nativeElement.clientWidth * 0.5; this.windowMouseMoveListener = - this.renderer.listenGlobal('window', 'mousemove', (e: MouseEvent) => { + this.renderer.listen('window', 'mousemove', (e: MouseEvent) => { this.onMouseMove(e); }); this.windowMouseUpListener = - this.renderer.listenGlobal('window', 'mouseup', () => { + this.renderer.listen('window', 'mouseup', () => { this.onMouseUp(); }); - this.renderer.setElementClass(this.thumb.nativeElement, 'focused', true); + this.renderer.addClass(this.thumb.nativeElement, 'focused'); this.isDragging = true; @@ -123,7 +123,7 @@ export class SliderComponent implements ControlValueAccessor { setTimeout(() => { this.releaseMouseHandlers(); - this.renderer.setElementClass(this.thumb.nativeElement, 'focused', false); + this.renderer.removeClass(this.thumb.nativeElement, 'focused'); }, 10); this.centerStartOffset = 0; @@ -168,7 +168,7 @@ export class SliderComponent implements ControlValueAccessor { private updateThumbPosition() { const relativeValue = Math.min(1, Math.max(0, (this.value - this.min) / (this.max - this.min))); - this.renderer.setElementStyle(this.thumb.nativeElement, 'left', relativeValue * 100 + '%'); + this.renderer.setStyle(this.thumb.nativeElement, 'left', relativeValue * 100 + '%'); } private releaseMouseHandlers() { diff --git a/src/Squidex/app/framework/angular/forms/slugify-input.directive.ts b/src/Squidex/app/framework/angular/forms/slugify-input.directive.ts deleted file mode 100644 index 0a77d763d..000000000 --- a/src/Squidex/app/framework/angular/forms/slugify-input.directive.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Directive, ElementRef, forwardRef, HostListener, Renderer } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import slugify from 'slugify'; - -import { Types } from '@app/framework/internal'; - -export const SQX_SLUGIFY_INPUT_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SlugifyInputDirective), multi: true -}; - -@Directive({ - selector: '[sqxSlugifyInput]', - providers: [SQX_SLUGIFY_INPUT_VALUE_ACCESSOR] -}) -export class SlugifyInputDirective implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - - constructor( - private readonly element: ElementRef, - private readonly renderer: Renderer - ) { - } - - @HostListener('input', ['$event.target.value']) - public onChange(value: any) { - const normalizedValue = this.transform(value); - - this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); - this.callChange(normalizedValue); - } - - @HostListener('blur') - public onTouched() { - this.callTouched(); - } - - public writeValue(value: string) { - const normalizedValue = this.transform(value); - - this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); - } - - public setDisabledState(isDisabled: boolean): void { - this.renderer.setElementProperty(this.element.nativeElement, 'disabled', isDisabled); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - - private transform(value: any): string { - return Types.isString(value) ? slugify(value, { lower: true }) : ''; - } -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/stars.component.ts b/src/Squidex/app/framework/angular/forms/stars.component.ts index 482fcf71d..0b3b0c728 100644 --- a/src/Squidex/app/framework/angular/forms/stars.component.ts +++ b/src/Squidex/app/framework/angular/forms/stars.component.ts @@ -51,13 +51,8 @@ export class StarsComponent implements ControlValueAccessor { public value: number | null = 1; - public writeValue(value: number | null | undefined) { - if (Types.isNumber(value)) { - this.value = this.stars = value || 0; - } else { - this.value = null; - this.stars = 0; - } + public writeValue(obj: any) { + this.value = this.stars = Types.isNumber(obj) ? obj : 0; } public setDisabledState(isDisabled: boolean): void { diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index 4fc1f4a71..6fd3fff59 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -88,11 +88,11 @@ export class TagEditorComponent implements ControlValueAccessor { public addInput = new FormControl(); - public writeValue(value: any[]) { + public writeValue(obj: any) { this.resetForm(); - if (this.converter && Types.isArrayOf(value, v => this.converter.isValidValue(v))) { - this.items = value; + if (this.converter && Types.isArrayOf(obj, v => this.converter.isValidValue(v))) { + this.items = obj; } else { this.items = []; } diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/src/Squidex/app/framework/angular/forms/toggle.component.ts index f9e6b4de4..270ef5eb8 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.ts +++ b/src/Squidex/app/framework/angular/forms/toggle.component.ts @@ -24,11 +24,11 @@ export class ToggleComponent implements ControlValueAccessor { private callChange = (v: any) => { /* NOOP */ }; private callTouched = () => { /* NOOP */ }; - public isChecked: boolean | null | undefined = null; + public isChecked: boolean | null = null; public isDisabled = false; - public writeValue(value: boolean | null | undefined) { - this.isChecked = Types.isBoolean(value) ? value : null; + public writeValue(obj: any) { + this.isChecked = Types.isBoolean(obj) ? obj : null; } public setDisabledState(isDisabled: boolean): void { diff --git a/src/Squidex/app/framework/angular/forms/transform-input.directive.ts b/src/Squidex/app/framework/angular/forms/transform-input.directive.ts new file mode 100644 index 000000000..891035990 --- /dev/null +++ b/src/Squidex/app/framework/angular/forms/transform-input.directive.ts @@ -0,0 +1,94 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Directive, ElementRef, forwardRef, HostListener, Input, Renderer2 } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +import slugify from 'slugify'; + +type Transform = (value: string) => string; + +export const TransformNoop: Transform = value => value; +export const TransformLowerCase: Transform = value => value.toLowerCase(); +export const TransformSlugify: Transform = value => slugify(value, { lower: true }); +export const TransformSlugifyCased: Transform = value => slugify(value, { lower: false }); +export const TransformUpperCase: Transform = value => value.toUpperCase(); + +import { Types } from '@app/framework/internal'; + +export const SQX_TRANSFORM_INPUT_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TransformInputDirective), multi: true +}; + +@Directive({ + selector: '[sqxTransformInput]', + providers: [SQX_TRANSFORM_INPUT_VALUE_ACCESSOR] +}) +export class TransformInputDirective implements ControlValueAccessor { + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; + private transformer: Transform; + + @Input('sqxTransformInput') + public set transform(value: Transform | string) { + if (Types.isString(value)) { + if (value === 'LowerCase') { + this.transformer = TransformLowerCase; + } else if (value === 'Slugify') { + this.transform = TransformSlugify; + } else if (value === 'SlugifyCased') { + this.transform = TransformSlugifyCased; + } else if (value === 'UpperCase') { + this.transform = TransformUpperCase; + } + } else { + this.transformer = value || TransformNoop; + } + } + + constructor( + private readonly element: ElementRef, + private readonly renderer: Renderer2 + ) { + } + + @HostListener('input', ['$event.target.value']) + public onChange(value: any) { + const normalizedValue = this.transformValue(value); + + this.renderer.setProperty(this.element.nativeElement, 'value', normalizedValue); + + this.callChange(normalizedValue); + } + + @HostListener('blur') + public onTouched() { + this.callTouched(); + } + + public writeValue(obj: any) { + const normalizedValue = this.transformValue(obj + ''); + + this.renderer.setProperty(this.element.nativeElement, 'value', normalizedValue); + } + + public setDisabledState(isDisabled: boolean): void { + this.renderer.setProperty(this.element.nativeElement, 'disabled', isDisabled); + } + + public registerOnChange(fn: any) { + this.callChange = fn; + } + + public registerOnTouched(fn: any) { + this.callTouched = fn; + } + + private transformValue(value: any): string { + return Types.isString(value) ? this.transformer(value) : ''; + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/validators.ts b/src/Squidex/app/framework/angular/forms/validators.ts index 7fe3d8615..fabd1bbc9 100644 --- a/src/Squidex/app/framework/angular/forms/validators.ts +++ b/src/Squidex/app/framework/angular/forms/validators.ts @@ -18,7 +18,7 @@ export module ValidatorsEx { let regeExp: RegExp; let regexStr: string; - if (typeof regex === 'string') { + if (Types.isString(regex)) { regexStr = `^${regex}$`; regeExp = new RegExp(regexStr); } else { diff --git a/src/Squidex/app/framework/angular/http/http-extensions-impl.ts b/src/Squidex/app/framework/angular/http/http-extensions-impl.ts index 498776a54..8fad99a21 100644 --- a/src/Squidex/app/framework/angular/http/http-extensions-impl.ts +++ b/src/Squidex/app/framework/angular/http/http-extensions-impl.ts @@ -9,7 +9,7 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpResponse } from '@angul import { Observable } from 'rxjs'; import { ErrorDto } from './../../utils/error'; - +import { Types} from './../../utils/types'; import { Version, Versioned } from './../../utils/version'; export module HTTP { @@ -64,9 +64,9 @@ export function pretifyError(message: string): Observable { return this.catch((response: HttpErrorResponse) => { let result: ErrorDto | null = null; - if (!(response.error instanceof Error)) { + if (!Types.is(response.error, Error)) { try { - const errorDto = typeof response.error === 'object' ? response.error : JSON.parse(response.error); + const errorDto = Types.isObject(response.error) ? response.error : JSON.parse(response.error); if (response.status === 412) { result = new ErrorDto(response.status, 'Failed to make the update. Another user has made a change. Please reload.'); diff --git a/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts b/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts index 65f09de73..384bcb25c 100644 --- a/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts +++ b/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, OnDestroy, OnInit, Renderer } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, OnDestroy, OnInit, Renderer2 } from '@angular/core'; @Directive({ selector: '[sqxIgnoreScrollbar]' @@ -18,7 +18,7 @@ export class IgnoreScrollbarDirective implements OnDestroy, OnInit, AfterViewIni constructor( private readonly element: ElementRef, - private readonly renderer: Renderer + private readonly renderer: Renderer2 ) { } @@ -30,7 +30,7 @@ export class IgnoreScrollbarDirective implements OnDestroy, OnInit, AfterViewIni public ngOnInit() { if (!this.parent) { - this.parent = this.element.nativeElement.parentElement; + this.parent = this.renderer.parentNode(this.element.nativeElement); } this.resizeListener = @@ -61,7 +61,7 @@ export class IgnoreScrollbarDirective implements OnDestroy, OnInit, AfterViewIni if (scrollbarWidth !== this.scollbarWidth) { this.scollbarWidth = scrollbarWidth; - this.renderer.setElementStyle(this.element.nativeElement, 'marginRight', `-${scrollbarWidth}px`); + this.renderer.setStyle(this.element.nativeElement, 'marginRight', `-${scrollbarWidth}px`); } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/image-source.directive.ts b/src/Squidex/app/framework/angular/image-source.directive.ts index 5cacfe5ba..752584091 100644 --- a/src/Squidex/app/framework/angular/image-source.directive.ts +++ b/src/Squidex/app/framework/angular/image-source.directive.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, Renderer } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { MathHelper } from './../utils/math-helper'; @@ -31,7 +31,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After constructor( private readonly element: ElementRef, - private readonly renderer: Renderer + private readonly renderer: Renderer2 ) { } @@ -43,7 +43,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After public ngOnInit() { if (!this.parent) { - this.parent = this.element.nativeElement.parentElement; + this.parent = this.renderer.parentNode(this.element.nativeElement); } this.parentResizeListener = @@ -65,12 +65,12 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After @HostListener('load') public onLoad() { - this.renderer.setElementStyle(this.element.nativeElement, 'visibility', 'visible'); + this.renderer.setStyle(this.element.nativeElement, 'visibility', 'visible'); } @HostListener('error') public onError() { - this.renderer.setElementStyle(this.element.nativeElement, 'visibility', 'hidden'); + this.renderer.setStyle(this.element.nativeElement, 'visibility', 'hidden'); this.retryLoadingImage(); } @@ -78,9 +78,9 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After private resize(parent: any) { this.size = this.parent.getBoundingClientRect(); - this.renderer.setElementStyle(this.element.nativeElement, 'display', 'inline-block'); - this.renderer.setElementStyle(this.element.nativeElement, 'width', this.size.width + 'px'); - this.renderer.setElementStyle(this.element.nativeElement, 'height', this.size.height + 'px'); + this.renderer.setStyle(this.element.nativeElement, 'display', 'inline-block'); + this.renderer.setStyle(this.element.nativeElement, 'width', this.size.width + 'px'); + this.renderer.setStyle(this.element.nativeElement, 'height', this.size.height + 'px'); this.setImageSource(); } @@ -100,7 +100,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After source += `&q=${this.loadQuery}`; } - this.renderer.setElementAttribute(this.element.nativeElement, 'src', source); + this.renderer.setProperty(this.element.nativeElement, 'src', source); } } diff --git a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts index cef07507f..3858649e1 100644 --- a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit, Renderer } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; const POSITION_TOPLEFT = 'topLeft'; const POSITION_TOPRIGHT = 'topRight'; @@ -39,7 +39,7 @@ export class ModalTargetDirective implements AfterViewInit, OnDestroy, OnInit { public autoPosition = true; constructor( - private readonly renderer: Renderer, + private readonly renderer: Renderer2, private readonly element: ElementRef ) { } @@ -80,8 +80,8 @@ export class ModalTargetDirective implements AfterViewInit, OnDestroy, OnInit { public ngAfterViewInit() { const modalRef = this.element.nativeElement; - this.renderer.setElementStyle(modalRef, 'position', 'fixed'); - this.renderer.setElementStyle(modalRef, 'z-index', '1000000'); + this.renderer.setStyle(modalRef, 'position', 'fixed'); + this.renderer.setStyle(modalRef, 'z-index', '1000000'); this.updatePosition(); } @@ -197,14 +197,14 @@ export class ModalTargetDirective implements AfterViewInit, OnDestroy, OnInit { const w = targetRect.width + 2 * this.offset; const h = targetRect.height + 2 * this.offset; - this.renderer.setElementStyle(modalRef, 'width', `${w}px`); - this.renderer.setElementStyle(modalRef, 'height', `${h}px`); + this.renderer.setStyle(modalRef, 'width', `${w}px`); + this.renderer.setStyle(modalRef, 'height', `${h}px`); } - this.renderer.setElementStyle(modalRef, 'top', `${t}px`); - this.renderer.setElementStyle(modalRef, 'left', `${l}px`); - this.renderer.setElementStyle(modalRef, 'right', 'auto'); - this.renderer.setElementStyle(modalRef, 'bottom', 'auto'); - this.renderer.setElementStyle(modalRef, 'margin', '0'); + this.renderer.setStyle(modalRef, 'top', `${t}px`); + this.renderer.setStyle(modalRef, 'left', `${l}px`); + this.renderer.setStyle(modalRef, 'right', 'auto'); + this.renderer.setStyle(modalRef, 'bottom', 'auto'); + this.renderer.setStyle(modalRef, 'margin', '0'); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts index c9421288e..895a8873f 100644 --- a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts @@ -5,10 +5,10 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Directive, EmbeddedViewRef, Input, OnChanges, OnDestroy, Renderer, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core'; +import { Directive, EmbeddedViewRef, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core'; import { Subscription } from 'rxjs'; -import { ModalView } from '@app/framework/internal'; +import { ModalView, Types } from '@app/framework/internal'; import { RootViewComponent } from './root-view.component'; @@ -31,7 +31,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { constructor( private readonly templateRef: TemplateRef, - private readonly renderer: Renderer, + private readonly renderer: Renderer2, private readonly viewContainer: ViewContainerRef, private readonly rootView: RootViewComponent ) { @@ -40,7 +40,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { public ngOnDestroy() { this.stopListening(); - if (this.modalView instanceof ModalView) { + if (Types.is(this.modalView, ModalView)) { this.modalView.hide(); } } @@ -55,7 +55,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { this.subscription = null; } - if (this.modalView instanceof ModalView) { + if (Types.is(this.modalView, ModalView)) { this.subscription = this.modalView.isOpen.subscribe(isOpen => { this.update(isOpen); @@ -78,7 +78,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { } if (this.renderedView.rootNodes[0].style) { - this.renderer.setElementStyle(this.renderedView.rootNodes[0], 'display', 'block'); + this.renderer.setStyle(this.renderedView.rootNodes[0], 'display', 'block'); } setTimeout(() => { @@ -103,7 +103,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { } this.documentClickListener = - this.renderer.listenGlobal('document', 'click', (event: MouseEvent) => { + this.renderer.listen('document', 'click', (event: MouseEvent) => { if (!event.target || this.renderedView === null) { return; } diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts b/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts index ad9538f75..ba3347821 100644 --- a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, Input, OnDestroy, OnInit, Renderer } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { fadeAnimation, @@ -43,7 +43,7 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit { constructor( private readonly onboardingService: OnboardingService, - private readonly renderer: Renderer + private readonly renderer: Renderer2 ) { } @@ -97,7 +97,7 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit { } if (this.for === underCursor) { return true; } else { - return this.isSameOrParent(underCursor.parentElement); + return this.isSameOrParent(this.renderer.parentNode(underCursor)); } } diff --git a/src/Squidex/app/framework/angular/modals/tooltip.component.ts b/src/Squidex/app/framework/angular/modals/tooltip.component.ts index 014ec1d9c..4bc1f20b5 100644 --- a/src/Squidex/app/framework/angular/modals/tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/tooltip.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, Renderer } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { ModalView } from './../../utils/modal-view'; @@ -30,7 +30,7 @@ export class TooltipComponent implements OnDestroy, OnInit { public modal = new ModalView(false, false); constructor( - private readonly renderer: Renderer + private readonly renderer: Renderer2 ) { } diff --git a/src/Squidex/app/framework/angular/panel-container.directive.ts b/src/Squidex/app/framework/angular/panel-container.directive.ts index 009a14bfd..4ddd78943 100644 --- a/src/Squidex/app/framework/angular/panel-container.directive.ts +++ b/src/Squidex/app/framework/angular/panel-container.directive.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, HostListener } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, HostListener, Renderer2 } from '@angular/core'; import { PanelComponent } from './panel.component'; @@ -17,7 +17,8 @@ export class PanelContainerDirective implements AfterViewInit { private containerWidth = 0; constructor( - private readonly element: ElementRef + private readonly element: ElementRef, + private readonly renderer: Renderer2 ) { } @@ -84,12 +85,8 @@ export class PanelContainerDirective implements AfterViewInit { currentLayer -= 10; } - const diff = currentPosition - this.containerWidth; + const diff = Math.max(0, currentPosition - this.containerWidth); - if (diff > 0) { - this.element.nativeElement.scrollLeft = diff; - } else { - this.element.nativeElement.scrollLeft = 0; - } + this.renderer.setProperty(this.element.nativeElement, 'scrollLeft', diff); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/src/Squidex/app/framework/angular/panel.component.ts index e76c52732..f0317de6a 100644 --- a/src/Squidex/app/framework/angular/panel.component.ts +++ b/src/Squidex/app/framework/angular/panel.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, Renderer, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; import { slideRightAnimation } from './animations'; @@ -59,7 +59,7 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { constructor( private readonly container: PanelContainerDirective, - private readonly renderer: Renderer + private readonly renderer: Renderer2 ) { } @@ -79,17 +79,17 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { if (this.styleWidth !== size) { this.styleWidth = size; - this.renderer.setElementStyle(this.panel.nativeElement, 'width', size); + this.renderer.setStyle(this.panel.nativeElement, 'width', size); this.renderWidth = this.panel.nativeElement.getBoundingClientRect().width; } } public arrange(left: any, layer: any) { - this.renderer.setElementStyle(this.panel.nativeElement, 'top', '0px'); - this.renderer.setElementStyle(this.panel.nativeElement, 'left', left); - this.renderer.setElementStyle(this.panel.nativeElement, 'bottom', '0px'); - this.renderer.setElementStyle(this.panel.nativeElement, 'position', 'absolute'); - this.renderer.setElementStyle(this.panel.nativeElement, 'z-index', layer); + this.renderer.setStyle(this.panel.nativeElement, 'top', '0px'); + this.renderer.setStyle(this.panel.nativeElement, 'left', left); + this.renderer.setStyle(this.panel.nativeElement, 'bottom', '0px'); + this.renderer.setStyle(this.panel.nativeElement, 'position', 'absolute'); + this.renderer.setStyle(this.panel.nativeElement, 'z-index', layer); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/routers/parent-link.directive.ts b/src/Squidex/app/framework/angular/routers/parent-link.directive.ts index 3dfd71d9a..9a093d53f 100644 --- a/src/Squidex/app/framework/angular/routers/parent-link.directive.ts +++ b/src/Squidex/app/framework/angular/routers/parent-link.directive.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Directive, ElementRef, HostListener, Input, OnDestroy, OnInit, Renderer } from '@angular/core'; +import { Directive, ElementRef, HostListener, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; @@ -23,7 +23,7 @@ export class ParentLinkDirective implements OnDestroy, OnInit { private readonly router: Router, private readonly route: ActivatedRoute, private readonly element: ElementRef, - private readonly renderer: Renderer + private readonly renderer: Renderer2 ) { } @@ -40,7 +40,7 @@ export class ParentLinkDirective implements OnDestroy, OnInit { this.router.createUrlTree(['.'], { relativeTo: this.route.parent!.parent }).toString() : this.router.createUrlTree(['.'], { relativeTo: this.route.parent }).toString(); - this.renderer.setElementAttribute(this.element.nativeElement, 'href', this.url); + this.renderer.setProperty(this.element.nativeElement, 'href', this.url); }); } diff --git a/src/Squidex/app/framework/angular/scroll-active.directive.ts b/src/Squidex/app/framework/angular/scroll-active.directive.ts index ac0fe898f..56a998c8a 100644 --- a/src/Squidex/app/framework/angular/scroll-active.directive.ts +++ b/src/Squidex/app/framework/angular/scroll-active.directive.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, Input, OnChanges } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, Input, OnChanges, Renderer2 } from '@angular/core'; @Directive({ selector: '[sqxScrollActive]' @@ -18,7 +18,8 @@ export class ScrollActiveDirective implements AfterViewInit, OnChanges { public container: HTMLElement; constructor( - private readonly element: ElementRef + private readonly element: ElementRef, + private readonly renderer: Renderer2 ) { } @@ -37,25 +38,23 @@ export class ScrollActiveDirective implements AfterViewInit, OnChanges { } private scrollInView(parent: HTMLElement, target: HTMLElement) { - if (!parent.getBoundingClientRect) { - return; - } - const parentRect = parent.getBoundingClientRect(); const targetRect = target.getBoundingClientRect(); - const offset = (targetRect.top + document.body.scrollTop) - (parentRect.top + document.body.scrollTop); + const body = document.body; + + const offset = (targetRect.top + body.scrollTop) - (parentRect.top + body.scrollTop); const scroll = parent.scrollTop; if (offset < 0) { - parent.scrollTop = scroll + offset; + this.renderer.setProperty(parent, 'scrollTop', scroll + offset); } else { const targetHeight = targetRect.height; const parentHeight = parentRect.height; if ((offset + targetHeight) > parentHeight) { - parent.scrollTop = scroll + offset - parentHeight + targetHeight; + this.renderer.setProperty(parent, 'scrollTop', scroll + offset - parentHeight + targetHeight); } } } diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index f285a2076..56fce1cfc 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -14,16 +14,16 @@ export * from './angular/forms/dropdown.component'; export * from './angular/forms/file-drop.directive'; export * from './angular/forms/focus-on-init.directive'; export * from './angular/forms/form-error.component'; +export * from './angular/forms/iframe-editor.component'; export * from './angular/forms/indeterminate-value.directive'; export * from './angular/forms/jscript-editor.component'; export * from './angular/forms/json-editor.component'; -export * from './angular/forms/lowercase-input.directive'; export * from './angular/forms/progress-bar.component'; export * from './angular/forms/slider.component'; -export * from './angular/forms/slugify-input.directive'; export * from './angular/forms/stars.component'; export * from './angular/forms/tag-editor.component'; export * from './angular/forms/toggle.component'; +export * from './angular/forms/transform-input.directive'; export * from './angular/forms/validators'; export * from './angular/http/http-extensions-impl'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index db3753c92..d1c67d92a 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -32,6 +32,7 @@ import { FormErrorComponent, FromNowPipe, FullDateTimePipe, + IFrameEditorComponent, IgnoreScrollbarDirective, ImageSourceDirective, IndeterminateValueDirective, @@ -41,7 +42,6 @@ import { KeysPipe, KNumberPipe, LocalStoreService, - LowerCaseInputDirective, MessageBus, ModalDialogComponent, ModalTargetDirective, @@ -64,7 +64,6 @@ import { ShortDatePipe, ShortTimePipe, SliderComponent, - SlugifyInputDirective, SortedDirective, StarsComponent, TagEditorComponent, @@ -73,6 +72,7 @@ import { TitleService, ToggleComponent, TooltipComponent, + TransformInputDirective, UserReportComponent } from './declarations'; @@ -101,6 +101,7 @@ import { FormErrorComponent, FromNowPipe, FullDateTimePipe, + IFrameEditorComponent, IgnoreScrollbarDirective, ImageSourceDirective, IndeterminateValueDirective, @@ -109,7 +110,6 @@ import { JsonEditorComponent, KeysPipe, KNumberPipe, - LowerCaseInputDirective, ModalDialogComponent, ModalTargetDirective, ModalViewDirective, @@ -128,7 +128,6 @@ import { ShortDatePipe, ShortTimePipe, SliderComponent, - SlugifyInputDirective, SortedDirective, StarsComponent, TagEditorComponent, @@ -136,6 +135,7 @@ import { TitleComponent, ToggleComponent, TooltipComponent, + TransformInputDirective, UserReportComponent ], exports: [ @@ -159,6 +159,7 @@ import { FormsModule, FromNowPipe, FullDateTimePipe, + IFrameEditorComponent, IgnoreScrollbarDirective, ImageSourceDirective, IndeterminateValueDirective, @@ -167,7 +168,6 @@ import { JsonEditorComponent, KeysPipe, KNumberPipe, - LowerCaseInputDirective, ModalDialogComponent, ModalTargetDirective, ModalViewDirective, @@ -187,7 +187,6 @@ import { ShortDatePipe, ShortTimePipe, SliderComponent, - SlugifyInputDirective, SortedDirective, StarsComponent, TagEditorComponent, @@ -195,6 +194,7 @@ import { TitleComponent, ToggleComponent, TooltipComponent, + TransformInputDirective, UserReportComponent ] }) diff --git a/src/Squidex/app/framework/services/analytics.service.ts b/src/Squidex/app/framework/services/analytics.service.ts index b68937b2c..e6ba06235 100644 --- a/src/Squidex/app/framework/services/analytics.service.ts +++ b/src/Squidex/app/framework/services/analytics.service.ts @@ -9,6 +9,7 @@ import { Injectable } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { AnalyticsIdConfig } from '../configurations'; +import { Types } from './../utils/types'; import { ResourceLoaderService } from './resource-loader.service'; // tslint:disable:only-arrow-functions @@ -31,7 +32,7 @@ export class AnalyticsService { if (analyticsId && router && resourceLoader && window.location.hostname !== 'localhost') { this.gtag('config', analyticsId.value); - router.events.filter(e => e instanceof NavigationEnd) + router.events.filter(e => Types.is(e, NavigationEnd)) .subscribe(() => { this.gtag('config', analyticsId.value, { page_path: window.location.pathname }); }); diff --git a/src/Squidex/app/framework/services/dialog.service.ts b/src/Squidex/app/framework/services/dialog.service.ts index 72b9a0e00..b7d77d543 100644 --- a/src/Squidex/app/framework/services/dialog.service.ts +++ b/src/Squidex/app/framework/services/dialog.service.ts @@ -9,6 +9,7 @@ import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { ErrorDto } from './../utils/error'; +import { Types } from './../utils/types'; export const DialogServiceFactory = () => { return new DialogService(); @@ -64,7 +65,7 @@ export class DialogService { } public notifyError(error: string | ErrorDto) { - if (error instanceof ErrorDto) { + if (Types.is(error, ErrorDto)) { this.notify(Notification.error(error.displayMessage)); } else { this.notify(Notification.error(error)); diff --git a/src/Squidex/app/framework/services/resource-loader.service.ts b/src/Squidex/app/framework/services/resource-loader.service.ts index c8a7fb039..601ef18e5 100644 --- a/src/Squidex/app/framework/services/resource-loader.service.ts +++ b/src/Squidex/app/framework/services/resource-loader.service.ts @@ -5,11 +5,16 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Injectable } from '@angular/core'; +import { Injectable, Renderer2, RendererFactory2 } from '@angular/core'; @Injectable() export class ResourceLoaderService { - private cache: { [path: string]: Promise } = {}; + private readonly cache: { [path: string]: Promise } = {}; + private readonly renderer: Renderer2; + + constructor(rendererFactory: RendererFactory2) { + this.renderer = rendererFactory.createRenderer(null, null); + } public loadStyle(url: string): Promise { const key = url.toUpperCase(); @@ -18,18 +23,13 @@ export class ResourceLoaderService { if (!result) { result = new Promise((resolve, reject) => { - const style = document.createElement('link'); - style.rel = 'stylesheet'; - style.href = url; - style.type = 'text/css'; - - style.onload = () => { - resolve(); - }; + const style = this.renderer.createElement('link'); - const head = document.getElementsByTagName('head')[0]; - - head.appendChild(style); + this.renderer.listen(style, 'load', () => resolve()); + this.renderer.setProperty(style, 'rel', 'stylesheet'); + this.renderer.setProperty(style, 'href', url); + this.renderer.setProperty(style, 'type', 'text/css'); + this.renderer.appendChild(document.head, style); }); this.cache[key] = result; @@ -45,19 +45,12 @@ export class ResourceLoaderService { if (!result) { result = new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = url; - script.async = true; - - script.onload = () => { - resolve(); - }; - - const node = document.getElementsByTagName('script')[0]; + const script = this.renderer.createElement('script'); - if (node.parentNode) { - node.parentNode.insertBefore(script, node); - } + this.renderer.listen(script, 'load', () => resolve()); + this.renderer.setProperty(script, 'src', url); + this.renderer.setProperty(script, 'async', true); + this.renderer.appendChild(document.body, script); }); this.cache[key] = result; diff --git a/src/Squidex/app/framework/state.ts b/src/Squidex/app/framework/state.ts index 03bca305b..a53b4c1e4 100644 --- a/src/Squidex/app/framework/state.ts +++ b/src/Squidex/app/framework/state.ts @@ -9,6 +9,7 @@ import { AbstractControl } from '@angular/forms'; import { BehaviorSubject, Observable } from 'rxjs'; import { ErrorDto } from './utils/error'; +import { Types } from './utils/types'; export interface FormState { submitted: boolean; @@ -69,7 +70,7 @@ export class Form { } private getError(error?: string | ErrorDto) { - if (error instanceof ErrorDto) { + if (Types.is(error, ErrorDto)) { return error.displayMessage; } else { return error; @@ -100,7 +101,7 @@ export class State { } public next(update: ((v: T) => T) | object) { - if (update instanceof Function) { + if (Types.isFunction(update)) { this.state.next(update(this.state.value)); } else { this.state.next(Object.assign({}, this.snapshot, update)); diff --git a/src/Squidex/app/framework/utils/types.spec.ts b/src/Squidex/app/framework/utils/types.spec.ts index 70fa846c8..54bd0a2f3 100644 --- a/src/Squidex/app/framework/utils/types.spec.ts +++ b/src/Squidex/app/framework/utils/types.spec.ts @@ -8,6 +8,13 @@ import { Types } from './types'; describe('Types', () => { + it('should calculate hash string', () => { + expect(Types.hash(null)).toBe('null'); + expect(Types.hash(undefined)).toBe(undefined); + + expect(Types.hash(new RegExp('.*'))).toEqual('{}'); + }); + it('should make string check', () => { expect(Types.isString('')).toBeTruthy(); expect(Types.isString('string')).toBeTruthy(); @@ -89,4 +96,23 @@ describe('Types', () => { expect(Types.isFunction([])).toBeFalsy(); }); -}); \ No newline at end of file + + it('should make type check', () => { + expect(Types.is(new MyClass(1), MyClass)).toBeTruthy(); + + expect(Types.is(1, MyClass)).toBeFalsy(); + }); + + it('should make json equals check', () => { + expect(Types.jsJsonEquals({ a: 1, b: 2 }, { a: 1, b: 2 })).toBeTruthy(); + + expect(Types.jsJsonEquals({ a: 1, b: 2 }, { b: 2, a: 1 })).toBeFalsy(); + }); +}); + +class MyClass { + constructor( + public readonly value: number + ) { + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/utils/types.ts b/src/Squidex/app/framework/utils/types.ts index a08609ee3..3c17ee8dd 100644 --- a/src/Squidex/app/framework/utils/types.ts +++ b/src/Squidex/app/framework/utils/types.ts @@ -6,51 +6,63 @@ */ export module Types { - export function isString(value: any): boolean { + export function hash(value: any): string { + try { + return JSON.stringify(value); + } catch (e) { + return ''; + } + } + + export function isString(value: any): value is string { return typeof value === 'string' || value instanceof String; } - export function isNumber(value: any): boolean { + export function isNumber(value: any): value is number { return typeof value === 'number' && isFinite(value); } - export function isArray(value: any): boolean { + export function isArray(value: any): value is Array { return Array.isArray(value); } - export function isFunction(value: any): boolean { + export function isFunction(value: any): value is Function { return typeof value === 'function'; } - export function isObject(value: any): boolean { + export function isObject(value: any): value is Object { return value && typeof value === 'object' && value.constructor === Object; } - export function isBoolean(value: any): boolean { + export function isBoolean(value: any): value is boolean { return typeof value === 'boolean'; } - export function isNull(value: any): boolean { + export function isNull(value: any): value is null { return value === null; } - export function isUndefined(value: any): boolean { + export function isUndefined(value: any): value is undefined { return typeof value === 'undefined'; } - export function isRegExp(value: any): boolean { + export function isRegExp(value: any): value is RegExp { return value && typeof value === 'object' && value.constructor === RegExp; } - export function isDate(value: any): boolean { + export function isDate(value: any): value is Date { return value instanceof Date; } - export function isArrayOfNumber(value: any): boolean { + export function is(x: any, c: new (...args: any[]) => TClass): x is TClass { + return x instanceof c; + } + + export function isArrayOfNumber(value: any): value is Array { return isArrayOf(value, v => isNumber(v)); } - export function isArrayOfString(value: any): boolean { + export function isArrayOfString(value: any): value is Array { return isArrayOf(value, v => isString(v)); } @@ -68,6 +80,10 @@ export module Types { return true; } + export function jsJsonEquals(lhs: T, rhs: T) { + return hash(lhs) === hash(rhs); + } + export function isEquals(lhs: T[], rhs: T[]) { if (!lhs && !rhs) { return true; diff --git a/src/Squidex/app/shared/components/app-form.component.html b/src/Squidex/app/shared/components/app-form.component.html index d4ba4ab14..24782a7d4 100644 --- a/src/Squidex/app/shared/components/app-form.component.html +++ b/src/Squidex/app/shared/components/app-form.component.html @@ -18,7 +18,7 @@ - + The app name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{createForm.appName | async}}/. diff --git a/src/Squidex/app/shared/components/asset.component.ts b/src/Squidex/app/shared/components/asset.component.ts index e9adb8739..919a56a30 100644 --- a/src/Squidex/app/shared/components/asset.component.ts +++ b/src/Squidex/app/shared/components/asset.component.ts @@ -18,6 +18,7 @@ import { fadeAnimation, ModalView, RenameAssetForm, + Types, UpdateAssetDto, Versioned } from '@app/shared/internal'; @@ -87,7 +88,7 @@ export class AssetComponent implements OnInit { if (initFile) { this.assetsService.uploadFile(this.appsState.appName, initFile, this.authState.user!.token, DateTime.now()) .subscribe(dto => { - if (dto instanceof AssetDto) { + if (Types.is(dto, AssetDto)) { this.emitLoaded(dto); } else { this.progress = dto; @@ -106,7 +107,7 @@ export class AssetComponent implements OnInit { if (files.length === 1) { this.assetsService.replaceFile(this.appsState.appName, this.asset.id, files[0], this.asset.version) .subscribe(dto => { - if (dto instanceof Versioned) { + if (Types.is(dto, Versioned)) { this.updateAsset(this.asset.update(dto.payload, this.authState.user!.token, dto.version), true); } else { this.setProgress(dto); diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.ts b/src/Squidex/app/shared/components/geolocation-editor.component.ts index f5e728e87..d64a39950 100644 --- a/src/Squidex/app/shared/components/geolocation-editor.component.ts +++ b/src/Squidex/app/shared/components/geolocation-editor.component.ts @@ -76,9 +76,9 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi ) { } - public writeValue(value: Geolocation) { - if (Types.isObject(value) && Types.isNumber(value.latitude) && Types.isNumber(value.longitude)) { - this.value = value; + public writeValue(obj: any) { + if (Types.isObject(obj) && Types.isNumber(obj.latitude) && Types.isNumber(obj.longitude)) { + this.value = obj; } else { this.value = null; } diff --git a/src/Squidex/app/shared/components/markdown-editor.component.ts b/src/Squidex/app/shared/components/markdown-editor.component.ts index 23b829f87..9540079be 100644 --- a/src/Squidex/app/shared/components/markdown-editor.component.ts +++ b/src/Squidex/app/shared/components/markdown-editor.component.ts @@ -5,14 +5,13 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, forwardRef, Renderer2, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { AssetDto, ModalView, - ResourceLoaderService, - Types + ResourceLoaderService } from '@app/shared/internal'; declare var SimpleMDE: any; @@ -48,13 +47,14 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI public isFullscreen = false; constructor( + private readonly renderer: Renderer2, private readonly resourceLoader: ResourceLoaderService ) { this.resourceLoader.loadStyle('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css'); } - public writeValue(value: string) { - this.value = Types.isString(value) ? value : ''; + public writeValue(obj: any) { + this.value = obj + ''; if (this.simplemde) { this.simplemde.value(this.value); @@ -179,11 +179,13 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI this.simplemde.codemirror.on('refresh', () => { this.isFullscreen = this.simplemde.isFullscreenActive(); + let target = this.container.nativeElement; + if (this.isFullscreen) { - document.body.appendChild(this.inner.nativeElement); - } else { - this.container.nativeElement.appendChild(this.inner.nativeElement); + target = document.body; } + + this.renderer.appendChild(target, this.inner.nativeElement); }); this.simplemde.codemirror.on('blur', () => { diff --git a/src/Squidex/app/shared/components/rich-editor.component.ts b/src/Squidex/app/shared/components/rich-editor.component.ts index 476d5d611..2512c325e 100644 --- a/src/Squidex/app/shared/components/rich-editor.component.ts +++ b/src/Squidex/app/shared/components/rich-editor.component.ts @@ -11,8 +11,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { AssetDto, ModalView, - ResourceLoaderService, - Types + ResourceLoaderService } from '@app/shared/internal'; declare var tinymce: any; @@ -112,8 +111,8 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, }; } - public writeValue(value: string) { - this.value = Types.isString(value) ? value : ''; + public writeValue(obj: any) { + this.value = obj + ''; if (this.tinyEditor) { this.tinyEditor.setContent(this.value); diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index 5b2b9e147..7f58faa6b 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/src/Squidex/app/shared/services/assets.service.ts @@ -14,6 +14,7 @@ import { ApiUrlConfig, DateTime, HTTP, + Types, Version, Versioned } from '@app/framework'; @@ -176,7 +177,7 @@ export class AssetsService { const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0; return percentDone; - } else if (event instanceof HttpResponse) { + } else if (Types.is(event, HttpResponse)) { const response: any = event.body; const assetUrl = this.apiUrl.buildUrl(`api/assets/${response.id}`); @@ -256,7 +257,7 @@ export class AssetsService { const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0; return percentDone; - } else if (event instanceof HttpResponse) { + } else if (Types.is(event, HttpResponse)) { const response: any = event.body; const replaced = new AssetReplacedDto( diff --git a/src/Squidex/app/shared/services/auth.service.ts b/src/Squidex/app/shared/services/auth.service.ts index b1f7ac20e..235c88461 100644 --- a/src/Squidex/app/shared/services/auth.service.ts +++ b/src/Squidex/app/shared/services/auth.service.ts @@ -15,7 +15,7 @@ import { WebStorageStateStore } from 'oidc-client'; -import { ApiUrlConfig } from '@app/framework'; +import { ApiUrlConfig, Types } from '@app/framework'; export class Profile { public get id(): string { @@ -163,7 +163,7 @@ export class AuthService { return observable.timeout(2000) .retryWhen(errors => errors - .mergeMap(e => e instanceof TimeoutError ? Observable.of(e) : Observable.throw(e)) + .mergeMap(e => Types.is(e, TimeoutError) ? Observable.of(e) : Observable.throw(e)) .delay(500) .take(5) .concat(Observable.throw(new Error('Retry limit exceeded.')))); diff --git a/src/Squidex/app/shared/services/schemas.fields.spec.ts b/src/Squidex/app/shared/services/schemas.fields.spec.ts index 2dda6b093..765989151 100644 --- a/src/Squidex/app/shared/services/schemas.fields.spec.ts +++ b/src/Squidex/app/shared/services/schemas.fields.spec.ts @@ -43,9 +43,9 @@ describe('SchemaDetailsDto', () => { }); it('should return configured fields as list fields if no schema field are declared', () => { - const field1 = createField(new AssetsFieldPropertiesDto(null, null, null, false, true), 1); - const field2 = createField(new AssetsFieldPropertiesDto(null, null, null, false, false), 2); - const field3 = createField(new AssetsFieldPropertiesDto(null, null, null, false, true), 3); + const field1 = createField(new AssetsFieldPropertiesDto(null, null, null, null, false, true), 1); + const field2 = createField(new AssetsFieldPropertiesDto(null, null, null, null, false, false), 2); + const field3 = createField(new AssetsFieldPropertiesDto(null, null, null, null, false, true), 3); const schema = createSchema(new SchemaPropertiesDto(''), 1, [field1, field2, field3]); @@ -53,9 +53,9 @@ describe('SchemaDetailsDto', () => { }); it('should return first fields as list fields if no schema field is declared', () => { - const field1 = createField(new AssetsFieldPropertiesDto(null, null, null, false, false), 1); - const field2 = createField(new AssetsFieldPropertiesDto(null, null, null, false, false), 2); - const field3 = createField(new AssetsFieldPropertiesDto(null, null, null, false, false), 3); + const field1 = createField(new AssetsFieldPropertiesDto(null, null, null, null, false, false), 1); + const field2 = createField(new AssetsFieldPropertiesDto(null, null, null, null, false, false), 2); + const field3 = createField(new AssetsFieldPropertiesDto(null, null, null, null, false, false), 3); const schema = createSchema(new SchemaPropertiesDto(''), 1, [field1, field2, field3]); @@ -71,50 +71,50 @@ describe('SchemaDetailsDto', () => { describe('FieldDto', () => { it('should return label as display name', () => { - const field = createField(new AssetsFieldPropertiesDto('Label', null, null, true, false), 1); + const field = createField(new AssetsFieldPropertiesDto('Label', null, null, null, true, false), 1); expect(field.displayName).toBe('Label'); }); it('should return name as display name if label is null', () => { - const field = createField(new AssetsFieldPropertiesDto(null, null, null, true, false), 1); + const field = createField(new AssetsFieldPropertiesDto(null, null, null, null, true, false), 1); expect(field.displayName).toBe('field1'); }); it('should return name as display name label is empty', () => { - const field = createField(new AssetsFieldPropertiesDto('', null, null, true, false), 1); + const field = createField(new AssetsFieldPropertiesDto('', null, null, null, true, false), 1); expect(field.displayName).toBe('field1'); }); it('should return placeholder as display placeholder', () => { - const field = createField(new AssetsFieldPropertiesDto(null, null, 'Placeholder', true, false), 1); + const field = createField(new AssetsFieldPropertiesDto(null, null, 'Placeholder', null, true, false), 1); expect(field.displayPlaceholder).toBe('Placeholder'); }); it('should return empty as display placeholder if placeholder is null', () => { - const field = createField(new AssetsFieldPropertiesDto(null, null, null, true, false)); + const field = createField(new AssetsFieldPropertiesDto(null, null, null, null, true, false)); expect(field.displayPlaceholder).toBe(''); }); it('should return localizable if partitioning is language', () => { - const field = createField(new AssetsFieldPropertiesDto(null, null, null, true, false), 1, 'language'); + const field = createField(new AssetsFieldPropertiesDto(null, null, null, null, true, false), 1, 'language'); expect(field.isLocalizable).toBeTruthy(); }); it('should not return localizable if partitioning is invarient', () => { - const field = createField(new AssetsFieldPropertiesDto(null, null, null, true, false), 1, 'invariant'); + const field = createField(new AssetsFieldPropertiesDto(null, null, null, null, true, false), 1, 'invariant'); expect(field.isLocalizable).toBeFalsy(); }); }); describe('AssetsField', () => { - const field = createField(new AssetsFieldPropertiesDto(null, null, null, true, false, 1, 1)); + const field = createField(new AssetsFieldPropertiesDto(null, null, null, null, true, false, 1, 1)); it('should create validators', () => { expect(field.createValidators(false).length).toBe(3); @@ -138,7 +138,7 @@ describe('AssetsField', () => { }); describe('TagsField', () => { - const field = createField(new TagsFieldPropertiesDto(null, null, null, true, false, 1, 1)); + const field = createField(new TagsFieldPropertiesDto(null, null, null, null, true, false, 1, 1)); it('should create validators', () => { expect(field.createValidators(false).length).toBe(3); @@ -162,7 +162,7 @@ describe('TagsField', () => { }); describe('BooleanField', () => { - const field = createField(new BooleanFieldPropertiesDto(null, null, null, true, false, false, 'Checkbox')); + const field = createField(new BooleanFieldPropertiesDto(null, null, null, null, true, false, false, 'Checkbox')); it('should create validators', () => { expect(field.createValidators(false).length).toBe(1); @@ -189,7 +189,7 @@ describe('BooleanField', () => { describe('DateTimeField', () => { const now = DateTime.parseISO_UTC('2017-10-12T16:30:10Z'); - const field = createField(new DateTimeFieldPropertiesDto(null, null, null, true, false, 'Date')); + const field = createField(new DateTimeFieldPropertiesDto(null, null, null, null, true, false, 'Date')); it('should create validators', () => { expect(field.createValidators(false).length).toBe(1); @@ -204,13 +204,13 @@ describe('DateTimeField', () => { }); it('should format to date', () => { - const dateField = createField(new DateTimeFieldPropertiesDto(null, null, null, true, false, 'Date')); + const dateField = createField(new DateTimeFieldPropertiesDto(null, null, null, null, true, false, 'Date')); expect(dateField.formatValue('2017-12-12T16:00:00Z')).toBe('2017-12-12'); }); it('should format to date time', () => { - const dateTimeField = createField(new DateTimeFieldPropertiesDto(null, null, null, true, false, 'DateTime')); + const dateTimeField = createField(new DateTimeFieldPropertiesDto(null, null, null, null, true, false, 'DateTime')); expect(dateTimeField.formatValue('2017-12-12T16:00:00Z')).toBe('2017-12-12 16:00:00'); }); @@ -235,7 +235,7 @@ describe('DateTimeField', () => { }); describe('GeolocationField', () => { - const field = createField(new GeolocationFieldPropertiesDto(null, null, null, true, false, 'Default')); + const field = createField(new GeolocationFieldPropertiesDto(null, null, null, null, true, false, 'Default')); it('should create validators', () => { expect(field.createValidators(false).length).toBe(1); @@ -255,7 +255,7 @@ describe('GeolocationField', () => { }); describe('JsonField', () => { - const field = createField(new JsonFieldPropertiesDto(null, null, null, true, false)); + const field = createField(new JsonFieldPropertiesDto(null, null, null, null, true, false)); it('should create validators', () => { expect(field.createValidators(false).length).toBe(1); @@ -275,7 +275,7 @@ describe('JsonField', () => { }); describe('NumberField', () => { - const field = createField(new NumberFieldPropertiesDto(null, null, null, true, false, false, 'Input', undefined, 3, 1, [1, 2, 3])); + const field = createField(new NumberFieldPropertiesDto(null, null, null, null, true, false, false, 'Input', undefined, 3, 1, [1, 2, 3])); it('should create validators', () => { expect(field.createValidators(false).length).toBe(4); @@ -297,7 +297,7 @@ describe('NumberField', () => { }); describe('ReferencesField', () => { - const field = createField(new ReferencesFieldPropertiesDto(null, null, null, true, false, 1, 1)); + const field = createField(new ReferencesFieldPropertiesDto(null, null, null, null, true, false, 1, 1)); it('should create validators', () => { expect(field.createValidators(false).length).toBe(3); @@ -321,7 +321,7 @@ describe('ReferencesField', () => { }); describe('StringField', () => { - const field = createField(new StringFieldPropertiesDto(null, null, null, true, false, false, 'Input', undefined, 'pattern', undefined, 3, 1, ['1', '2'])); + const field = createField(new StringFieldPropertiesDto(null, null, null, null, true, false, false, 'Input', undefined, 'pattern', undefined, 3, 1, ['1', '2'])); it('should create validators', () => { expect(field.createValidators(false).length).toBe(5); diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 492318838..0e154710c 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -61,31 +61,31 @@ export function createProperties(fieldType: string, values: Object | null = null switch (fieldType) { case 'Number': - properties = new NumberFieldPropertiesDto(null, null, null, false, false, false, 'Input'); + properties = new NumberFieldPropertiesDto(null, null, null, null, false, false, false, 'Input'); break; case 'String': - properties = new StringFieldPropertiesDto(null, null, null, false, false, false, 'Input'); + properties = new StringFieldPropertiesDto(null, null, null, null, false, false, false, 'Input'); break; case 'Boolean': - properties = new BooleanFieldPropertiesDto(null, null, null, false, false, false, 'Checkbox'); + properties = new BooleanFieldPropertiesDto(null, null, null, null, false, false, false, 'Checkbox'); break; case 'DateTime': - properties = new DateTimeFieldPropertiesDto(null, null, null, false, false, 'DateTime'); + properties = new DateTimeFieldPropertiesDto(null, null, null, null, false, false, 'DateTime'); break; case 'Geolocation': - properties = new GeolocationFieldPropertiesDto(null, null, null, false, false, 'Map'); + properties = new GeolocationFieldPropertiesDto(null, null, null, null, false, false, 'Map'); break; case 'Json': - properties = new JsonFieldPropertiesDto(null, null, null, false, false); + properties = new JsonFieldPropertiesDto(null, null, null, null, false, false); break; case 'References': - properties = new ReferencesFieldPropertiesDto(null, null, null, false, false); + properties = new ReferencesFieldPropertiesDto(null, null, null, null, false, false); break; case 'Assets': - properties = new AssetsFieldPropertiesDto(null, null, null, false, false); + properties = new AssetsFieldPropertiesDto(null, null, null, null, false, false); break; case 'Tags': - properties = new TagsFieldPropertiesDto(null, null, null, false, false); + properties = new TagsFieldPropertiesDto(null, null, null, null, false, false); break; default: throw 'Invalid properties type'; @@ -176,6 +176,7 @@ export abstract class FieldPropertiesDto { public readonly label: string | null, public readonly hints: string | null, public readonly placeholder: string | null, + public readonly editorUrl: string | null, public readonly isRequired: boolean, public readonly isListField: boolean ) { @@ -191,7 +192,7 @@ export abstract class FieldPropertiesDto { } export class StringFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string | null, hints: string | null, placeholder: string | null, + constructor(label: string | null, hints: string | null, placeholder: string | null, editorUrl: string | null, isRequired: boolean, isListField: boolean, public readonly inlineEditable: boolean, @@ -203,7 +204,7 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto { public readonly maxLength?: number, public readonly allowedValues?: string[] ) { - super('String', label, hints, placeholder, isRequired, isListField); + super('String', label, hints, placeholder, editorUrl, isRequired, isListField); } public formatValue(value: any): string { @@ -252,7 +253,7 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto { } export class NumberFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string | null, hints: string | null, placeholder: string | null, + constructor(label: string | null, hints: string | null, placeholder: string | null, editorUrl: string | null, isRequired: boolean, isListField: boolean, public readonly inlineEditable: boolean, @@ -262,7 +263,7 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto { public readonly minValue?: number, public readonly allowedValues?: number[] ) { - super('Number', label, hints, placeholder, isRequired, isListField); + super('Number', label, hints, placeholder, editorUrl, isRequired, isListField); } public formatValue(value: any): string { @@ -307,7 +308,7 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto { } export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string | null, hints: string | null, placeholder: string | null, + constructor(label: string | null, hints: string | null, placeholder: string | null, editorUrl: string | null, isRequired: boolean, isListField: boolean, public readonly editor: string, @@ -316,7 +317,7 @@ export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { public readonly minValue?: string, public readonly calculatedDefaultValue?: string ) { - super('DateTime', label, hints, placeholder, isRequired, isListField); + super('DateTime', label, hints, placeholder, editorUrl, isRequired, isListField); } public formatValue(value: any): string { @@ -361,14 +362,14 @@ export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { } export class BooleanFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string | null, hints: string | null, placeholder: string | null, + constructor(label: string | null, hints: string | null, placeholder: string | null, editorUrl: string | null, isRequired: boolean, isListField: boolean, public readonly inlineEditable: boolean, public readonly editor: string, public readonly defaultValue?: boolean ) { - super('Boolean', label, hints, placeholder, isRequired, isListField); + super('Boolean', label, hints, placeholder, editorUrl, isRequired, isListField); } public formatValue(value: any): string { @@ -395,12 +396,12 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto { } export class GeolocationFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string | null, hints: string | null, placeholder: string | null, + constructor(label: string | null, hints: string | null, placeholder: string | null, editorUrl: string | null, isRequired: boolean, isListField: boolean, public readonly editor: string ) { - super('Geolocation', label, hints, placeholder, isRequired, isListField); + super('Geolocation', label, hints, placeholder, editorUrl, isRequired, isListField); } public formatValue(value: any): string { @@ -423,14 +424,14 @@ export class GeolocationFieldPropertiesDto extends FieldPropertiesDto { } export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string | null, hints: string | null, placeholder: string | null, + constructor(label: string | null, hints: string | null, placeholder: string | null, editorUrl: string | null, isRequired: boolean, isListField: boolean, public readonly minItems?: number, public readonly maxItems?: number, public readonly schemaId?: string ) { - super('References', label, hints, placeholder, isRequired, isListField); + super('References', label, hints, placeholder, editorUrl, isRequired, isListField); } public formatValue(value: any): string { @@ -465,7 +466,7 @@ export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { } export class AssetsFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string | null, hints: string | null, placeholder: string | null, + constructor(label: string | null, hints: string | null, placeholder: string | null, editorUrl: string | null, isRequired: boolean, isListField: boolean, public readonly minItems?: number, @@ -481,7 +482,7 @@ export class AssetsFieldPropertiesDto extends FieldPropertiesDto { public readonly aspectWidth?: number, public readonly aspectHeight?: number ) { - super('Assets', label, hints, placeholder, isRequired, isListField); + super('Assets', label, hints, placeholder, editorUrl, isRequired, isListField); } public formatValue(value: any): string { @@ -516,13 +517,13 @@ export class AssetsFieldPropertiesDto extends FieldPropertiesDto { } export class TagsFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string | null, hints: string | null, placeholder: string | null, + constructor(label: string | null, hints: string | null, placeholder: string | null, editorUrl: string | null, isRequired: boolean, isListField: boolean, public readonly minItems?: number, public readonly maxItems?: number ) { - super('Tags', label, hints, placeholder, isRequired, isListField); + super('Tags', label, hints, placeholder, editorUrl, isRequired, isListField); } public formatValue(value: any): string { @@ -557,11 +558,11 @@ export class TagsFieldPropertiesDto extends FieldPropertiesDto { } export class JsonFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string | null, hints: string | null, placeholder: string | null, + constructor(label: string | null, hints: string | null, placeholder: string | null, editorUrl: string | null, isRequired: boolean, isListField: boolean ) { - super('Json', label, hints, placeholder, isRequired, isListField); + super('Json', label, hints, placeholder, editorUrl, isRequired, isListField); } public formatValue(value: any): string { diff --git a/src/Squidex/app/shared/state/schemas.state.ts b/src/Squidex/app/shared/state/schemas.state.ts index e397b0d15..ec1a9ba9f 100644 --- a/src/Squidex/app/shared/state/schemas.state.ts +++ b/src/Squidex/app/shared/state/schemas.state.ts @@ -17,6 +17,7 @@ import { Form, ImmutableArray, State, + Types, ValidatorsEx, Version } from '@app/framework'; @@ -85,6 +86,12 @@ export class EditFieldForm extends Form { Validators.maxLength(1000) ] ], + placeholder: ['', + [ + Validators.maxLength(1000) + ] + ], + editorUrl: null, isRequired: false, isListField: false })); @@ -364,7 +371,7 @@ export class SchemasState extends State { } const setPublished = (schema: SchemaDto | SchemaDetailsDto, publish: boolean, user: string, version: Version, now?: DateTime) => { - if (schema instanceof SchemaDetailsDto) { + if (Types.is(schema, SchemaDetailsDto)) { return new SchemaDetailsDto( schema.id, schema.name, diff --git a/src/Squidex/app/theme/_lists.scss b/src/Squidex/app/theme/_lists.scss index 9d58f63c0..ab2afbf5c 100644 --- a/src/Squidex/app/theme/_lists.scss +++ b/src/Squidex/app/theme/_lists.scss @@ -156,9 +156,6 @@ &-edit-button { & { color: $color-theme-blue; - line-height: 1rem; - font-size: 1rem; - font-weight: normal; padding-left: 0; padding-right: 0; width: 2.2rem; diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 8a75d41fe..7c1e57286 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -177,7 +177,7 @@ "identity": { /* - * Enable password auth. + * Enable password auth. Set this to false if you want to disable local login, leaving only 3rd party login options. */ "allowPasswordAuth": true, /* diff --git a/src/Squidex/wwwroot/scripts/editor-sdk.js b/src/Squidex/wwwroot/scripts/editor-sdk.js new file mode 100644 index 000000000..97c1eb9e4 --- /dev/null +++ b/src/Squidex/wwwroot/scripts/editor-sdk.js @@ -0,0 +1,100 @@ + +function SquidexFormField() { + var disabledHandler; + var disabled; + var valueHandler; + var value; + var timer; + var height = document.body.offsetHeight; + + function eventListener(event) { + if (event.source !== window) { + if (event.data.type === 'disabled') { + disabled = event.data.isDisabled; + + if (disabledHandler) { + disabledHandler(disabled); + } + } else if (event.data.type === 'valueChanged') { + var value = event.data.value; + + if (valueHandler) { + valueHandler(value); + } + } + } + } + + document.body.style.margin = '0'; + document.body.style.padding = '0'; + + window.addEventListener('message', eventListener, false); + + window.parent.postMessage({ type: 'started' }, '*'); + window.parent.postMessage({ type: 'resize', height: height }, '*'); + + timer = setInterval(function () { + var newHeight = document.body.offsetHeight; + + if (height !== newHeight) { + height = newHeight; + + window.parent.postMessage({ type: 'resize', height: height }, '*') + } + }, 500); + + var editor = { + /* + * Notifies the control container that the editor has been touched. + */ + touched: function () { + if (window.parent) { + window.parent.postMessage({ type: 'touched' }, '*'); + } + }, + + /* + * Notifies the control container that the value has been changed. + */ + valueChanged: function (value) { + if (window.parent) { + window.parent.postMessage({ type: 'valueChanged', value: value }, '*'); + } + }, + + /* + * Register the disabled handler. + */ + onDisabled: function (callback) { + disabledHandler = callback; + + if (callback) { + callback(disabled); + } + }, + + /* + * Register the disabled handler. + */ + onValueChanged: function (callback) { + valueHandler = callback; + + if (callback) { + callback(value); + } + }, + + /* + * Clean the editor SDK. + */ + clean: function () { + if (timer) { + window.removeEventListener('message', eventListener); + + timer(); + } + } + }; + + return editor; +}; \ No newline at end of file diff --git a/src/Squidex/wwwroot/scripts/simple-editor.html b/src/Squidex/wwwroot/scripts/simple-editor.html new file mode 100644 index 000000000..e9075c248 --- /dev/null +++ b/src/Squidex/wwwroot/scripts/simple-editor.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 8f314e5aa..4dd9c0b59 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -79,88 +79,88 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules } [Fact] - public void Should_replace_app_information_from_event() + public async Task Should_replace_app_information_from_event() { var @event = new ContentCreated { AppId = appId }; - var result = sut.FormatString("Name $APP_NAME has id $APP_ID", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("Name $APP_NAME has id $APP_ID", AsEnvelope(@event)); Assert.Equal($"Name my-app has id {appId.Id}", result); } [Fact] - public void Should_replace_schema_information_from_event() + public async Task Should_replace_schema_information_from_event() { var @event = new ContentCreated { SchemaId = schemaId }; - var result = sut.FormatString("Name $SCHEMA_NAME has id $SCHEMA_ID", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("Name $SCHEMA_NAME has id $SCHEMA_ID", AsEnvelope(@event)); Assert.Equal($"Name my-schema has id {schemaId.Id}", result); } [Fact] - public void Should_replace_timestamp_information_from_event() + public async Task Should_replace_timestamp_information_from_event() { var now = DateTime.UtcNow; var envelope = AsEnvelope(new ContentCreated()).SetTimestamp(Instant.FromDateTimeUtc(now)); - var result = sut.FormatString("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope); + var result = await sut.FormatStringAsync("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope); Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result); } [Fact] - public void Should_format_email_and_display_name_from_user() + public async Task Should_format_email_and_display_name_from_user() { A.CallTo(() => userResolver.FindByIdOrEmailAsync("123")) .Returns(user); var @event = new ContentCreated { Actor = new RefToken("subject", "123") }; - var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); Assert.Equal($"From me (me@email.com)", result); } [Fact] - public void Should_return_undefined_if_user_is_not_found() + public async Task Should_return_undefined_if_user_is_not_found() { A.CallTo(() => userResolver.FindByIdOrEmailAsync("123")) .Returns(Task.FromResult(null)); var @event = new ContentCreated { Actor = new RefToken("subject", "123") }; - var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); Assert.Equal($"From UNDEFINED (UNDEFINED)", result); } [Fact] - public void Should_return_undefined_if_user_failed_to_resolve() + public async Task Should_return_undefined_if_user_failed_to_resolve() { A.CallTo(() => userResolver.FindByIdOrEmailAsync("123")) .Throws(new InvalidOperationException()); var @event = new ContentCreated { Actor = new RefToken("subject", "123") }; - var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); Assert.Equal($"From UNDEFINED (UNDEFINED)", result); } [Fact] - public void Should_format_email_and_display_name_from_client() + public async Task Should_format_email_and_display_name_from_client() { var @event = new ContentCreated { Actor = new RefToken("client", "android") }; - var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); Assert.Equal($"From client:android (client:android)", result); } [Fact] - public void Should_replacecontent_url_from_event() + public async Task Should_replace_content_url_from_event() { var url = "http://content"; @@ -169,13 +169,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var @event = new ContentCreated { AppId = appId, ContentId = contentId, SchemaId = schemaId }; - var result = sut.FormatString("Go to $CONTENT_URL", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("Go to $CONTENT_URL", AsEnvelope(@event)); Assert.Equal($"Go to {url}", result); } [Fact] - public void Should_return_undefined_when_field_not_found() + public async Task Should_return_undefined_when_field_not_found() { var @event = new ContentCreated { @@ -186,13 +186,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.FormatString("$CONTENT_DATA.country.iv", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.country.iv", AsEnvelope(@event)); Assert.Equal("UNDEFINED", result); } [Fact] - public void Should_return_undefined_when_partition_not_found() + public async Task Should_return_undefined_when_partition_not_found() { var @event = new ContentCreated { @@ -203,13 +203,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.FormatString("$CONTENT_DATA.city.de", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.de", AsEnvelope(@event)); Assert.Equal("UNDEFINED", result); } [Fact] - public void Should_return_undefined_when_array_item_not_found() + public async Task Should_return_undefined_when_array_item_not_found() { var @event = new ContentCreated { @@ -220,13 +220,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", new JArray())) }; - var result = sut.FormatString("$CONTENT_DATA.city.de.10", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.de.10", AsEnvelope(@event)); Assert.Equal("UNDEFINED", result); } [Fact] - public void Should_return_undefined_when_property_not_found() + public async Task Should_return_undefined_when_property_not_found() { var @event = new ContentCreated { @@ -238,13 +238,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules new JProperty("name", "Berlin")))) }; - var result = sut.FormatString("$CONTENT_DATA.city.de.Name", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.de.Name", AsEnvelope(@event)); Assert.Equal("UNDEFINED", result); } [Fact] - public void Should_return_plain_value_when_found() + public async Task Should_return_plain_value_when_found() { var @event = new ContentCreated { @@ -255,13 +255,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event)); Assert.Equal("Berlin", result); } [Fact] - public void Should_return_plain_value_when_found_from_update_event() + public async Task Should_return_plain_value_when_found_from_update_event() { var @event = new ContentUpdated { @@ -272,13 +272,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event)); Assert.Equal("Berlin", result); } [Fact] - public void Should_return_undefined_when_null() + public async Task Should_return_undefined_when_null() { var @event = new ContentCreated { @@ -289,13 +289,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", JValue.CreateNull())) }; - var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event)); Assert.Equal("UNDEFINED", result); } [Fact] - public void Should_return_undefined_when_undefined() + public async Task Should_return_undefined_when_undefined() { var @event = new ContentCreated { @@ -306,13 +306,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", JValue.CreateUndefined())) }; - var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event)); Assert.Equal("UNDEFINED", result); } [Fact] - public void Should_return_string_when_object() + public async Task Should_return_string_when_object() { var @event = new ContentCreated { @@ -324,13 +324,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules new JProperty("name", "Berlin")))) }; - var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv", AsEnvelope(@event)); Assert.Equal(JObject.FromObject(new { name = "Berlin" }).ToString(Formatting.Indented), result); } [Fact] - public void Should_return_plain_value_from_array_when_found() + public async Task Should_return_plain_value_from_array_when_found() { var @event = new ContentCreated { @@ -342,13 +342,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules "Berlin"))) }; - var result = sut.FormatString("$CONTENT_DATA.city.iv.0", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv.0", AsEnvelope(@event)); Assert.Equal("Berlin", result); } [Fact] - public void Should_return_plain_value_from_object_when_found() + public async Task Should_return_plain_value_from_object_when_found() { var @event = new ContentCreated { @@ -360,19 +360,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules new JProperty("name", "Berlin")))) }; - var result = sut.FormatString("$CONTENT_DATA.city.iv.name", AsEnvelope(@event)); + var result = await sut.FormatStringAsync("$CONTENT_DATA.city.iv.name", AsEnvelope(@event)); Assert.Equal("Berlin", result); } [Fact] - public void Should_format_content_actions_when_found() + public async Task Should_format_content_actions_when_found() { - Assert.Equal("created", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentCreated()))); - Assert.Equal("updated", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentUpdated()))); - Assert.Equal("deleted", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentDeleted()))); + Assert.Equal("created", await sut.FormatStringAsync("$CONTENT_ACTION", AsEnvelope(new ContentCreated()))); + Assert.Equal("updated", await sut.FormatStringAsync("$CONTENT_ACTION", AsEnvelope(new ContentUpdated()))); + Assert.Equal("deleted", await sut.FormatStringAsync("$CONTENT_ACTION", AsEnvelope(new ContentDeleted()))); - Assert.Equal("set to archived", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentStatusChanged { Status = Status.Archived }))); + Assert.Equal("set to archived", await sut.FormatStringAsync("$CONTENT_ACTION", AsEnvelope(new ContentStatusChanged { Status = Status.Archived }))); } private static Envelope AsEnvelope(T @event) where T : AppEvent diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 57122aa8c..8fecceb38 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -8,6 +8,7 @@ using System; using System.Threading.Tasks; using FakeItEasy; +using Newtonsoft.Json.Linq; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; @@ -66,40 +67,40 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules } [Fact] - public void Should_not_create_job_for_invalid_event() + public async Task Should_not_create_job_for_invalid_event() { var ruleConfig = new Rule(new ContentChangedTrigger(), new WebhookAction()); var ruleEnvelope = Envelope.Create(new InvalidEvent()); - var job = sut.CreateJob(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); Assert.Null(job); } [Fact] - public void Should_not_create_job_if_no_trigger_handler_registered() + public async Task Should_not_create_job_if_no_trigger_handler_registered() { var ruleConfig = new Rule(new InvalidTrigger(), new WebhookAction()); var ruleEnvelope = Envelope.Create(new ContentCreated()); - var job = sut.CreateJob(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); Assert.Null(job); } [Fact] - public void Should_not_create_job_if_no_action_handler_registered() + public async Task Should_not_create_job_if_no_action_handler_registered() { var ruleConfig = new Rule(new ContentChangedTrigger(), new InvalidAction()); var ruleEnvelope = Envelope.Create(new ContentCreated()); - var job = sut.CreateJob(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); Assert.Null(job); } [Fact] - public void Should_not_create_if_not_triggered() + public async Task Should_not_create_if_not_triggered() { var ruleConfig = new Rule(new ContentChangedTrigger(), new WebhookAction()); var ruleEnvelope = Envelope.Create(new ContentCreated()); @@ -107,13 +108,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.Triggers(A>.Ignored, ruleConfig.Trigger)) .Returns(false); - var job = sut.CreateJob(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); Assert.Null(job); } [Fact] - public void Should_not_create_job_if_too_old() + public async Task Should_not_create_job_if_too_old() { var e = new ContentCreated { SchemaId = new NamedId(Guid.NewGuid(), "my-schema"), AppId = new NamedId(Guid.NewGuid(), "my-event") }; @@ -124,7 +125,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules ruleEnvelope.SetTimestamp(now.Minus(Duration.FromDays(3))); - var actionData = new RuleJobData(); + var actionData = new JObject(); var actionDescription = "MyDescription"; var eventName = "MySchemaCreatedEvent"; @@ -135,16 +136,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.Triggers(A>.Ignored, ruleConfig.Trigger)) .Returns(true); - A.CallTo(() => ruleActionHandler.CreateJob(A>.Ignored, eventName, ruleConfig.Action)) + A.CallTo(() => ruleActionHandler.CreateJobAsync(A>.Ignored, eventName, ruleConfig.Action)) .Returns((actionDescription, actionData)); - var job = sut.CreateJob(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); Assert.Null(job); } [Fact] - public void Should_create_job_if_triggered() + public async Task Should_create_job_if_triggered() { var e = new ContentCreated { SchemaId = new NamedId(Guid.NewGuid(), "my-schema"), AppId = new NamedId(Guid.NewGuid(), "my-event") }; @@ -156,7 +157,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules ruleEnvelope.SetTimestamp(now); var actionName = "WebhookAction"; - var actionData = new RuleJobData(); + var actionData = new JObject(); var actionDescription = "MyDescription"; var eventName = "MySchemaCreatedEvent"; @@ -167,10 +168,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.Triggers(A>.Ignored, ruleConfig.Trigger)) .Returns(true); - A.CallTo(() => ruleActionHandler.CreateJob(A>.Ignored, eventName, ruleConfig.Action)) + A.CallTo(() => ruleActionHandler.CreateJobAsync(A>.Ignored, eventName, ruleConfig.Action)) .Returns((actionDescription, actionData)); - var job = sut.CreateJob(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); Assert.Equal(eventName, job.EventName); @@ -189,7 +190,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_return_succeeded_job_with_full_dump_when_handler_returns_no_exception() { - var ruleJob = new RuleJobData(); + var ruleJob = new JObject(); var ruleEx = new InvalidOperationException(); var actionDump = "MyDump"; @@ -208,7 +209,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_return_failed_job_with_full_dump_when_handler_returns_exception() { - var ruleJob = new RuleJobData(); + var ruleJob = new JObject(); var ruleEx = new InvalidOperationException(); var actionDump = "MyDump"; @@ -227,7 +228,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_return_timedout_job_with_full_dump_when_exception_from_handler_indicates_timeout() { - var ruleJob = new RuleJobData(); + var ruleJob = new JObject(); var ruleEx = new InvalidOperationException(); var actionDump = "MyDump"; @@ -247,7 +248,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_create_exception_details_when_job_to_execute_failed() { - var ruleJob = new RuleJobData(); + var ruleJob = new JObject(); var ruleEx = new InvalidOperationException(); A.CallTo(() => ruleActionHandler.ExecuteJobAsync(ruleJob)) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexCommandMiddlewareTests.cs new file mode 100644 index 000000000..8d67c852f --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexCommandMiddlewareTests.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByNameIndexCommandMiddlewareTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly IAppsByNameIndex index = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly AppsByNameIndexCommandMiddleware sut; + + public AppsByNameIndexCommandMiddlewareTests() + { + A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) + .Returns(index); + + sut = new AppsByNameIndexCommandMiddleware(grainFactory); + } + + [Fact] + public async Task Should_add_app_to_index_on_create() + { + var context = + new CommandContext(new CreateApp { AppId = appId, Name = "my-app" }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAppAsync(appId, "my-app")) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_app_from_index_on_archive() + { + var context = + new CommandContext(new ArchiveApp { AppId = appId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.RemoveAppAsync(appId)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs new file mode 100644 index 000000000..5db614a21 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByNameIndexGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); + private readonly Guid appId1 = Guid.NewGuid(); + private readonly Guid appId2 = Guid.NewGuid(); + private readonly string appName1 = "my-app1"; + private readonly string appName2 = "my-app2"; + private readonly AppsByNameIndexGrain sut; + + public AppsByNameIndexGrainTests() + { + A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + .Returns(persistence); + + sut = new AppsByNameIndexGrain(store); + sut.OnActivateAsync(SingleGrain.Id).Wait(); + } + + [Fact] + public async Task Should_add_app_id_to_index() + { + await sut.AddAppAsync(appId1, appName1); + + var result = await sut.GetAppIdAsync(appName1); + + Assert.Equal(appId1, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_app_id_from_index() + { + await sut.AddAppAsync(appId1, appName1); + await sut.RemoveAppAsync(appId1); + + var result = await sut.GetAppIdAsync(appName1); + + Assert.Equal(Guid.Empty, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_replace_app_ids_on_rebuild() + { + var state = new Dictionary + { + [appName1] = appId1, + [appName2] = appId2 + }; + + await sut.RebuildAsync(state); + + Assert.Equal(appId1, await sut.GetAppIdAsync(appName1)); + Assert.Equal(appId2, await sut.GetAppIdAsync(appName2)); + + Assert.Equal(new List { appId1, appId2 }, await sut.GetAppIdsAsync()); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs new file mode 100644 index 000000000..afa3851b2 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByUserIndexCommandMiddlewareTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly IAppsByUserIndex index = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly string userId = "123"; + private readonly AppsByUserIndexCommandMiddleware sut; + + public AppsByUserIndexCommandMiddlewareTests() + { + A.CallTo(() => grainFactory.GetGrain(userId, null)) + .Returns(index); + + sut = new AppsByUserIndexCommandMiddleware(grainFactory); + } + + [Fact] + public async Task Should_add_app_to_index_on_create() + { + var context = + new CommandContext(new CreateApp { AppId = appId, Actor = new RefToken("user", userId) }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAppAsync(appId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_add_app_to_index_on_assign_of_contributor() + { + var context = + new CommandContext(new AssignContributor { AppId = appId }, commandBus) + .Complete(EntityCreatedResult.Create(userId, 1)); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAppAsync(appId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_add_app_to_index_on_remove_of_contributor() + { + var context = + new CommandContext(new RemoveContributor { AppId = appId, ContributorId = userId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.RemoveAppAsync(appId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_app_from_index_on_archive() + { + var appGrain = A.Fake(); + var appState = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain(appId, null)) + .Returns(appGrain); + + A.CallTo(() => appGrain.GetStateAsync()) + .Returns(J.AsTask(appState)); + + A.CallTo(() => appState.Contributors) + .Returns(AppContributors.Empty.Assign(userId, AppContributorPermission.Owner)); + + var context = + new CommandContext(new ArchiveApp { AppId = appId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.RemoveAppAsync(appId)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs new file mode 100644 index 000000000..087b6f63f --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.States; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByUserIndexGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); + private readonly Guid appId1 = Guid.NewGuid(); + private readonly Guid appId2 = Guid.NewGuid(); + private readonly AppsByUserIndexGrain sut; + + public AppsByUserIndexGrainTests() + { + A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + .Returns(persistence); + + sut = new AppsByUserIndexGrain(store); + sut.OnActivateAsync("user").Wait(); + } + + [Fact] + public async Task Should_add_app_id_to_index() + { + await sut.AddAppAsync(appId1); + await sut.AddAppAsync(appId2); + + var result = await sut.GetAppIdsAsync(); + + Assert.Equal(new List { appId1, appId2 }, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_remove_app_id_from_index() + { + await sut.AddAppAsync(appId1); + await sut.AddAppAsync(appId2); + await sut.RemoveAppAsync(appId1); + + var result = await sut.GetAppIdsAsync(); + + Assert.Equal(new List { appId2 }, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappenedTwiceOrMore(); + } + + [Fact] + public async Task Should_replace_app_ids_on_rebuild() + { + var state = new HashSet + { + appId1, + appId2 + }; + + await sut.RebuildAsync(state); + + var result = await sut.GetAppIdsAsync(); + + Assert.Equal(new List { appId1, appId2 }, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexCommandMiddlewareTests.cs new file mode 100644 index 000000000..30fada75b --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexCommandMiddlewareTests.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public sealed class RulesByAppIndexCommandMiddlewareTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly IRulesByAppIndex index = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly Guid ruleId = Guid.NewGuid(); + private readonly RulesByAppIndexCommandMiddleware sut; + + public RulesByAppIndexCommandMiddlewareTests() + { + A.CallTo(() => grainFactory.GetGrain(appId, null)) + .Returns(index); + + sut = new RulesByAppIndexCommandMiddleware(grainFactory); + } + + [Fact] + public async Task Should_add_rule_to_index_on_create() + { + var context = + new CommandContext(new CreateRule { RuleId = appId, AppId = new NamedId(appId, "my-app") }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddRuleAsync(appId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_rule_from_index_on_delete() + { + var ruleGrain = A.Fake(); + var ruleState = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain(appId, null)) + .Returns(ruleGrain); + + A.CallTo(() => ruleGrain.GetStateAsync()) + .Returns(J.AsTask(ruleState)); + + A.CallTo(() => ruleState.AppId) + .Returns(new NamedId(appId, "my-app")); + + var context = + new CommandContext(new DeleteRule { RuleId = appId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.RemoveRuleAsync(appId)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs new file mode 100644 index 000000000..be15f041f --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.States; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public sealed class RulesByAppIndexGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); + private readonly Guid ruleId1 = Guid.NewGuid(); + private readonly Guid ruleId2 = Guid.NewGuid(); + private readonly RulesByAppIndexGrain sut; + + public RulesByAppIndexGrainTests() + { + A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + .Returns(persistence); + + sut = new RulesByAppIndexGrain(store); + sut.OnActivateAsync(Guid.NewGuid()).Wait(); + } + + [Fact] + public async Task Should_add_rule_id_to_index() + { + await sut.AddRuleAsync(ruleId1); + await sut.AddRuleAsync(ruleId2); + + var result = await sut.GetRuleIdsAsync(); + + Assert.Equal(new List { ruleId1, ruleId2 }, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_remove_rule_id_from_index() + { + await sut.AddRuleAsync(ruleId1); + await sut.AddRuleAsync(ruleId2); + await sut.RemoveRuleAsync(ruleId1); + + var result = await sut.GetRuleIdsAsync(); + + Assert.Equal(new List { ruleId2 }, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappenedTwiceOrMore(); + } + + [Fact] + public async Task Should_replace_rule_ids_on_rebuild() + { + var state = new HashSet + { + ruleId1, + ruleId2 + }; + + await sut.RebuildAsync(state); + + var result = await sut.GetRuleIdsAsync(); + + Assert.Equal(new List { ruleId1, ruleId2 }, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs index a8f1bfd1c..ac2ef6ea7 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs @@ -8,6 +8,7 @@ using System; using System.Threading.Tasks; using FakeItEasy; +using Newtonsoft.Json.Linq; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; @@ -49,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Rules [InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult result, RuleJobResult jobResult) { - var actionData = new RuleJobData(); + var actionData = new JObject(); var actionName = "MyAction"; var @event = CreateEvent(calls, actionName, actionData); @@ -73,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Rules .MustHaveHappened(); } - private IRuleEventEntity CreateEvent(int numCalls, string actionName, RuleJobData actionData) + private IRuleEventEntity CreateEvent(int numCalls, string actionName, JObject actionData) { var @event = A.Fake(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 97f58f0b3..de5eed0bb 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -80,14 +80,14 @@ namespace Squidex.Domain.Apps.Entities.Rules A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) .Returns(new List { ruleEntity1, ruleEntity2, ruleEntity3 }); - A.CallTo(() => ruleService.CreateJob(rule1, @event)) + A.CallTo(() => ruleService.CreateJobAsync(rule1, @event)) .Returns(job1); - A.CallTo(() => ruleService.CreateJob(rule2, @event)) + A.CallTo(() => ruleService.CreateJobAsync(rule2, @event)) .Returns(job2); - A.CallTo(() => ruleService.CreateJob(rule3, @event)) - .Returns(null); + A.CallTo(() => ruleService.CreateJobAsync(rule3, @event)) + .Returns(Task.FromResult(null)); await sut.On(@event); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs new file mode 100644 index 000000000..32fe82c5c --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public sealed class SchemasByAppIndexCommandMiddlewareTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly ISchemasByAppIndex index = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly SchemasByAppIndexCommandMiddleware sut; + + public SchemasByAppIndexCommandMiddlewareTests() + { + A.CallTo(() => grainFactory.GetGrain(appId, null)) + .Returns(index); + + sut = new SchemasByAppIndexCommandMiddleware(grainFactory); + } + + [Fact] + public async Task Should_add_schema_to_index_on_create() + { + var context = + new CommandContext(new CreateSchema { SchemaId = appId, Name = "my-schema", AppId = new NamedId(appId, "my-app") }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddSchemaAsync(appId, "my-schema")) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_schema_from_index_on_delete() + { + var schemaGrain = A.Fake(); + var schemaState = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain(appId, null)) + .Returns(schemaGrain); + + A.CallTo(() => schemaGrain.GetStateAsync()) + .Returns(J.AsTask(schemaState)); + + A.CallTo(() => schemaState.AppId) + .Returns(new NamedId(appId, "my-app")); + + var context = + new CommandContext(new DeleteSchema { SchemaId = appId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.RemoveSchemaAsync(appId)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs new file mode 100644 index 000000000..5da814102 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs @@ -0,0 +1,83 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.States; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public sealed class SchemasByAppIndexGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); + private readonly Guid schemaId1 = Guid.NewGuid(); + private readonly Guid schemaId2 = Guid.NewGuid(); + private readonly string schemaName1 = "my-schema1"; + private readonly string schemaName2 = "my-schema2"; + private readonly SchemasByAppIndexGrain sut; + + public SchemasByAppIndexGrainTests() + { + A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + .Returns(persistence); + + sut = new SchemasByAppIndexGrain(store); + sut.OnActivateAsync(Guid.NewGuid()).Wait(); + } + + [Fact] + public async Task Should_add_schema_id_to_index() + { + await sut.AddSchemaAsync(schemaId1, schemaName1); + + var result = await sut.GetSchemaIdAsync(schemaName1); + + Assert.Equal(schemaId1, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_schema_id_from_index() + { + await sut.AddSchemaAsync(schemaId1, schemaName1); + await sut.RemoveSchemaAsync(schemaId1); + + var result = await sut.GetSchemaIdAsync(schemaName1); + + Assert.Equal(Guid.Empty, result); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_replace_schema_ids_on_rebuild() + { + var state = new Dictionary + { + [schemaName1] = schemaId1, + [schemaName2] = schemaId2 + }; + + await sut.RebuildAsync(state); + + Assert.Equal(schemaId1, await sut.GetSchemaIdAsync(schemaName1)); + Assert.Equal(schemaId2, await sut.GetSchemaIdAsync(schemaName2)); + + Assert.Equal(new List { schemaId1, schemaId2 }, await sut.GetSchemaIdsAsync()); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs index 4c3e54e11..16645f738 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs @@ -7,6 +7,7 @@ using FakeItEasy; using Orleans; +using Squidex.Infrastructure.Orleans; using Xunit; namespace Squidex.Infrastructure.EventSourcing.Grains @@ -20,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { var factory = A.Fake(); - A.CallTo(() => factory.GetGrain("Default", null)) + A.CallTo(() => factory.GetGrain(SingleGrain.Id, null)) .Returns(manager); sut = new OrleansEventNotifier(factory); diff --git a/tools/Migrate_01/MigrationPath.cs b/tools/Migrate_01/MigrationPath.cs index 81eb97d0d..4fa33b273 100644 --- a/tools/Migrate_01/MigrationPath.cs +++ b/tools/Migrate_01/MigrationPath.cs @@ -15,7 +15,7 @@ namespace Migrate_01 { public sealed class MigrationPath : IMigrationPath { - private const int CurrentVersion = 8; + private const int CurrentVersion = 10; private readonly IServiceProvider serviceProvider; public MigrationPath(IServiceProvider serviceProvider) @@ -32,6 +32,17 @@ namespace Migrate_01 var migrations = new List(); + // Version 10: Delete old archive fields + if (version < 10) + { + var migration = serviceProvider.GetService(); + + if (migration != null) + { + migrations.Add(migration); + } + } + // Version 6: Convert Event store. Must always be executed first. if (version < 6) { @@ -50,13 +61,30 @@ namespace Migrate_01 migrations.Add(serviceProvider.GetRequiredService()); } + // Version 9: Grain Indexes + if (version < 9) + { + var migration = serviceProvider.GetService(); + + if (migration != null) + { + migrations.Add(migration); + } + + migrations.Add(serviceProvider.GetRequiredService()); + } + // Version 1: Introduce App patterns. if (version <= 1) { migrations.Add(serviceProvider.GetRequiredService()); } - migrations.Add(serviceProvider.GetRequiredService()); + // Version 8: Introduce Archive collection. + if (version < 8) + { + migrations.Add(serviceProvider.GetRequiredService()); + } return (CurrentVersion, migrations); } diff --git a/tools/Migrate_01/Migrations/AddPatterns.cs b/tools/Migrate_01/Migrations/AddPatterns.cs index f07b2d20f..aa060fb99 100644 --- a/tools/Migrate_01/Migrations/AddPatterns.cs +++ b/tools/Migrate_01/Migrations/AddPatterns.cs @@ -10,8 +10,8 @@ using System.Threading.Tasks; using Orleans; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Orleans; namespace Migrate_01.Migrations { @@ -19,18 +19,16 @@ namespace Migrate_01.Migrations { private readonly InitialPatterns initialPatterns; private readonly IGrainFactory grainFactory; - private readonly IAppRepository appRepository; - public AddPatterns(InitialPatterns initialPatterns, IAppRepository appRepository, IGrainFactory grainFactory) + public AddPatterns(InitialPatterns initialPatterns, IGrainFactory grainFactory) { this.initialPatterns = initialPatterns; - this.appRepository = appRepository; this.grainFactory = grainFactory; } public async Task UpdateAsync() { - var ids = await appRepository.QueryAppIdsAsync(); + var ids = await grainFactory.GetGrain(SingleGrain.Id).GetAppIdsAsync(); foreach (var id in ids) { diff --git a/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs b/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs new file mode 100644 index 000000000..4528f8e5b --- /dev/null +++ b/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01.Migrations +{ + public sealed class ConvertOldSnapshotStores : IMigration + { + private readonly IMongoDatabase database; + + public ConvertOldSnapshotStores(IMongoDatabase database) + { + this.database = database; + } + + public Task UpdateAsync() + { + var collections = new[] + { + "States_Apps", + "States_Rules", + "States_Schemas" + }; + + var update = Builders.Update.Rename("State", "Doc"); + + var filter = new BsonDocument(); + + return Task.WhenAll( + collections + .Select(x => database.GetCollection(x)) + .Select(x => x.UpdateManyAsync(filter, update))); + } + } +} diff --git a/tools/Migrate_01/Migrations/DeleteArchiveCollectionSetup.cs b/tools/Migrate_01/Migrations/DeleteArchiveCollectionSetup.cs new file mode 100644 index 000000000..02b6d2608 --- /dev/null +++ b/tools/Migrate_01/Migrations/DeleteArchiveCollectionSetup.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01.Migrations +{ + public sealed class DeleteArchiveCollectionSetup : IMigration + { + private readonly IMongoDatabase database; + + public DeleteArchiveCollectionSetup(IMongoDatabase database) + { + this.database = database; + } + + public async Task UpdateAsync() + { + var collection = database.GetCollection("States_Contents"); + + await collection.Indexes.DropAllAsync(); + await collection.UpdateManyAsync(new BsonDocument(), Builders.Update.Unset("id")); + } + } +} diff --git a/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs b/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs new file mode 100644 index 000000000..40828a5fa --- /dev/null +++ b/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs @@ -0,0 +1,121 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Migrate_01.Migrations +{ + public class PopulateGrainIndexes : IMigration + { + private readonly IGrainFactory grainFactory; + private readonly ISnapshotStore statesForApps; + private readonly ISnapshotStore statesForRules; + private readonly ISnapshotStore statesForSchemas; + + public PopulateGrainIndexes( + IGrainFactory grainFactory, + ISnapshotStore statesForApps, + ISnapshotStore statesForRules, + ISnapshotStore statesForSchemas) + { + this.grainFactory = grainFactory; + this.statesForApps = statesForApps; + this.statesForRules = statesForRules; + this.statesForSchemas = statesForSchemas; + } + + public Task UpdateAsync() + { + return Task.WhenAll( + RebuildAppIndexes(), + RebuildRuleIndexes(), + RebuildSchemaIndexes()); + } + + private async Task RebuildAppIndexes() + { + var appsByName = new Dictionary(); + var appsByUser = new Dictionary>(); + + await statesForApps.ReadAllAsync((app, version) => + { + if (!app.IsArchived) + { + appsByName[app.Name] = app.Id; + + foreach (var contributor in app.Contributors.Keys) + { + appsByUser.GetOrAddNew(contributor).Add(app.Id); + } + } + + return TaskHelper.Done; + }); + + await grainFactory.GetGrain(SingleGrain.Id).RebuildAsync(appsByName); + + foreach (var kvp in appsByUser) + { + await grainFactory.GetGrain(kvp.Key).RebuildAsync(kvp.Value); + } + } + + private async Task RebuildRuleIndexes() + { + var rulesByApp = new Dictionary>(); + + await statesForRules.ReadAllAsync((schema, version) => + { + if (!schema.IsDeleted) + { + rulesByApp.GetOrAddNew(schema.AppId.Id).Add(schema.Id); + } + + return TaskHelper.Done; + }); + + foreach (var kvp in rulesByApp) + { + await grainFactory.GetGrain(kvp.Key).RebuildAsync(kvp.Value); + } + } + + private async Task RebuildSchemaIndexes() + { + var schemasByApp = new Dictionary>(); + + await statesForSchemas.ReadAllAsync((schema, version) => + { + if (!schema.IsDeleted) + { + schemasByApp.GetOrAddNew(schema.AppId.Id)[schema.Name] = schema.Id; + } + + return TaskHelper.Done; + }); + + foreach (var kvp in schemasByApp) + { + await grainFactory.GetGrain(kvp.Key).RebuildAsync(kvp.Value); + } + } + } +} diff --git a/tools/Migrate_01/Rebuilder.cs b/tools/Migrate_01/Rebuilder.cs index 7d27b1861..ea14482cc 100644 --- a/tools/Migrate_01/Rebuilder.cs +++ b/tools/Migrate_01/Rebuilder.cs @@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.State; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.State; @@ -138,22 +139,13 @@ namespace Migrate_01 { var @event = ParseKnownEvent(storedEvent); - if (@event.Payload is ContentEvent contentEvent) + if (@event.Payload is ContentEvent contentEvent && handledIds.Add(contentEvent.ContentId)) { try { - var (content, version) = await snapshotContentStore.ReadAsync(contentEvent.ContentId); + var content = grainFactory.GetGrain(contentEvent.ContentId); - if (content == null) - { - version = EtagVersion.Empty; - - content = new ContentState(); - } - - content = content.Apply(@event); - - await snapshotContentStore.WriteAsync(contentEvent.ContentId, content, version, version + 1); + await content.WriteSnapshotAsync(); } catch (DomainObjectNotFoundException) {