Browse Source

Merge branch 'master' into feature-proposal

# Conflicts:
#	src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs
#	src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
pull/285/head
Sebastian 9 years ago
parent
commit
4a78b03127
  1. 19
      CHANGELOG.md
  2. 2
      Dockerfile
  3. 3
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
  4. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  5. 97
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
  6. 52
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs
  7. 108
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs
  8. 65
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs
  9. 70
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs
  10. 69
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
  11. 5
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs
  12. 22
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs
  13. 16
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  14. 7
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  15. 44
      src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs
  16. 64
      src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs
  17. 44
      src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository_SnapshotStore.cs
  18. 5
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  19. 5
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  20. 14
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  21. 17
      src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs
  22. 41
      src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs
  23. 45
      src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs
  24. 42
      src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs
  25. 45
      src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs
  26. 54
      src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs
  27. 43
      src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs
  28. 31
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  29. 2
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  30. 36
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  31. 47
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs
  32. 80
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
  33. 80
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs
  34. 73
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs
  35. 17
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs
  36. 15
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs
  37. 5
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  38. 2
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  39. 4
      src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  40. 2
      src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
  41. 3
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  42. 15
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs
  43. 56
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs
  44. 73
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs
  45. 2
      src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  46. 2
      src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  47. 5
      src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs
  48. 27
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs
  49. 56
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs
  50. 80
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs
  51. 2
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  52. 5
      src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
  53. 13
      src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  54. 1
      src/Squidex.Infrastructure.MongoDb/States/MongoState.cs
  55. 1
      src/Squidex.Infrastructure/CachingProviderBase.cs
  56. 4
      src/Squidex.Infrastructure/Commands/CommandContext.cs
  57. 3
      src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs
  58. 2
      src/Squidex.Infrastructure/Orleans/Bootstrap.cs
  59. 11
      src/Squidex.Infrastructure/Orleans/J.cs
  60. 10
      src/Squidex.Infrastructure/Orleans/SingleGrain.cs
  61. 18
      src/Squidex.Infrastructure/States/CollectionNameAttribute.cs
  62. 2
      src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs
  63. 3
      src/Squidex.Infrastructure/States/ISnapshotStore.cs
  64. 3
      src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
  65. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs
  66. 1
      src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  67. 33
      src/Squidex/Config/Domain/EntitiesServices.cs
  68. 74
      src/Squidex/Config/Domain/StoreServices.cs
  69. 3
      src/Squidex/Config/Orleans/SiloWrapper.cs
  70. 2
      src/Squidex/Pipeline/Swagger/SwaggerHelper.cs
  71. 1
      src/Squidex/Program.cs
  72. 194
      src/Squidex/app/features/content/pages/content/content-field.component.html
  73. 32
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  74. 2
      src/Squidex/app/features/content/shared/content-item.component.html
  75. 32
      src/Squidex/app/features/content/shared/references-editor.component.ts
  76. 2
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  77. 7
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts
  78. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss
  79. 11
      src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html
  80. 5
      src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts
  81. 11
      src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html
  82. 5
      src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts
  83. 11
      src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html
  84. 12
      src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html
  85. 6
      src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss
  86. 11
      src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html
  87. 5
      src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts
  88. 11
      src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html
  89. 5
      src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts
  90. 2
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html
  91. 2
      src/Squidex/app/features/settings/pages/clients/clients-page.component.html
  92. 3
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  93. 8
      src/Squidex/app/framework/angular/forms/autocomplete.component.ts
  94. 6
      src/Squidex/app/framework/angular/forms/control-errors.component.ts
  95. 4
      src/Squidex/app/framework/angular/forms/copy.directive.ts
  96. 12
      src/Squidex/app/framework/angular/forms/date-time-editor.component.ts
  97. 4
      src/Squidex/app/framework/angular/forms/dropdown.component.ts
  98. 8
      src/Squidex/app/framework/angular/forms/file-drop.directive.ts
  99. 48
      src/Squidex/app/framework/angular/forms/focus-on-init.directive.spec.ts
  100. 15
      src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts

19
CHANGELOG.md

@ -1,10 +1,27 @@
# Changelog # 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 ## v1.3.0 - 2018-02-17
### Features ### Features
* **Actions**: ElasticSearch action * **Actions**: ElasticSearch action.
### Refactorings ### Refactorings

2
Dockerfile

@ -38,7 +38,7 @@ FROM microsoft/aspnetcore:2.0.3-jessie
# Default AspNetCore directory # Default AspNetCore directory
WORKDIR /app WORKDIR /app
# Copy from nuild stage # Copy from build stage
COPY --from=builder /out/ . COPY --from=builder /out/ .
EXPOSE 80 EXPOSE 80

3
src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Newtonsoft.Json.Linq;
using NodaTime; using NodaTime;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
@ -28,6 +29,6 @@ namespace Squidex.Domain.Apps.Core.Rules
public Instant Expires { get; set; } public Instant Expires { get; set; }
public RuleJobData ActionData { get; set; } public JObject ActionData { get; set; }
} }
} }

2
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 Placeholder { get; set; }
public string EditorUrl { get; set; }
public abstract T Accept<T>(IFieldPropertiesVisitor<T> visitor); public abstract T Accept<T>(IFieldPropertiesVisitor<T> visitor);
public abstract Field CreateField(long id, string name, Partitioning partitioning); public abstract Field CreateField(long id, string name, Partitioning partitioning);

97
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs

@ -11,17 +11,32 @@ using Algolia.Search;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
public sealed class AlgoliaActionHandler : RuleActionHandler<AlgoliaAction> 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<AlgoliaAction, AlgoliaJob>
{ {
private const string DescriptionIgnore = "Ignore";
private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients; private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients;
private readonly RuleEventFormatter formatter; private readonly RuleEventFormatter formatter;
@ -39,20 +54,20 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
}); });
} }
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, AlgoliaAction action) protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(Envelope<AppEvent> @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) if (@event.Payload is ContentEvent contentEvent)
{ {
ruleData["ContentId"] = contentEvent.ContentId.ToString(); var contentId = contentEvent.ContentId.ToString();
ruleData["Operation"] = "Upsert";
ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event); 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(); 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}"; ruleDescription = $"Add entry to Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject( ruleJob.Content = new JObject(
new JProperty("id", contentEvent.ContentId), new JProperty("objectID", contentId),
new JProperty("id", contentId),
new JProperty("created", timestamp), new JProperty("created", timestamp),
new JProperty("createdBy", created.Actor.ToString()), new JProperty("createdBy", created.Actor.ToString()),
new JProperty("lastModified", timestamp), new JProperty("lastModified", timestamp),
@ -77,7 +93,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
ruleDescription = $"Update entry in Algolia index: {action.IndexName}"; 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("lastModified", timestamp),
new JProperty("lastModifiedBy", updated.Actor.ToString()), new JProperty("lastModifiedBy", updated.Actor.ToString()),
new JProperty("data", formatter.ToRouteData(updated.Data))); 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}"; 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("lastModified", timestamp),
new JProperty("lastModifiedBy", statusChanged.Actor.ToString()), new JProperty("lastModifiedBy", statusChanged.Actor.ToString()),
new JProperty("status", statusChanged.Status.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}"; ruleDescription = $"Delete entry from Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject();
ruleData["Operation"] = "Delete";
break; 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.")); return (null, new InvalidOperationException("The action cannot handle this event."));
} }
var appId = job["AppId"].Value<string>(); var index = clients.GetClient((job.AppId, job.ApiKey, job.IndexName));
var apiKey = job["ApiKey"].Value<string>();
var indexName = job["IndexName"].Value<string>();
var index = clients.GetClient((appId, apiKey, indexName));
var operation = operationToken.Value<string>();
var content = job["Content"].Value<JObject>();
var contentId = job["ContentId"].Value<string>();
try try
{ {
switch (operation) if (job.Content != null)
{ {
case "Upsert": var response = await index.PartialUpdateObjectAsync(job.Content);
{
content["objectID"] = contentId;
var response = await index.PartialUpdateObjectAsync(content);
return (response.ToString(Formatting.Indented), null);
}
case "Delete":
{
var response = await index.DeleteObjectAsync(contentId);
return (response.ToString(Formatting.Indented), null); return (response.ToString(Formatting.Indented), null);
} }
else
{
var response = await index.DeleteObjectAsync(job.ContentId);
default: return (response.ToString(Formatting.Indented), null);
return (null, null);
} }
} }
catch (AlgoliaException ex) catch (AlgoliaException ex)

52
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs

@ -11,15 +11,34 @@ using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue; using Microsoft.WindowsAzure.Storage.Queue;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
public sealed class AzureQueueActionHandler : RuleActionHandler<AzureQueueAction> 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<AzureQueueAction, AzureQueueJob>
{ {
private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients; private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients;
private readonly RuleEventFormatter formatter; private readonly RuleEventFormatter formatter;
@ -41,31 +60,28 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
}); });
} }
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, AzureQueueAction action) protected override async Task<(string Description, AzureQueueJob Data)> CreateJobAsync(Envelope<AppEvent> @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 queueName = await formatter.FormatStringAsync(action.Queue, @event);
var ruleData = new RuleJobData
var ruleDescription = $"Send AzureQueueJob to azure queue '{action.Queue}'";
var ruleJob = new AzureQueueJob
{ {
["QueueConnectionString"] = action.ConnectionString, QueueConnectionString = action.ConnectionString,
["QueueName"] = action.Queue, QueueName = queueName,
["MessageBody"] = body 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<string>(); var queue = clients.GetClient((job.QueueConnectionString, job.QueueName));
var queueName = job["QueueName"].Value<string>();
var queue = clients.GetClient((queueConnectionString, queueName));
var messageBody = job["MessageBody"].ToString(Formatting.Indented);
await queue.AddMessageAsync(new CloudQueueMessage(messageBody)); await queue.AddMessageAsync(new CloudQueueMessage(job.Body));
return ("Completed", null); return ("Completed", null);
} }

108
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs

@ -10,17 +10,37 @@ using System.Threading.Tasks;
using Elasticsearch.Net; using Elasticsearch.Net;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
public sealed class ElasticSearchActionHandler : RuleActionHandler<ElasticSearchAction> 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<ElasticSearchAction, ElasticSearchJob>
{ {
private const string DescriptionIgnore = "Ignore";
private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients; private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients;
private readonly RuleEventFormatter formatter; private readonly RuleEventFormatter formatter;
@ -43,37 +63,40 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
}); });
} }
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, ElasticSearchAction action) protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(Envelope<AppEvent> @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) if (@event.Payload is ContentEvent contentEvent)
{ {
ruleData["ContentId"] = contentEvent.ContentId.ToString(); var contentId = contentEvent.ContentId.ToString();
ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event);
ruleData["IndexType"] = formatter.FormatString(action.IndexType, @event); 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 timestamp = @event.Headers.Timestamp().ToString();
var actor = @event.Payload.Actor.ToString();
switch (@event.Payload) switch (@event.Payload)
{ {
case ContentCreated created: case ContentCreated created:
{ {
ruleDescription = $"Add entry to ES index: {action.IndexName}"; ruleDescription = $"Add entry to ES index: {action.IndexName}";
ruleData["Operation"] = "Create"; ruleJob.Operation = "Create";
ruleData["Content"] = new JObject( ruleJob.Content = new JObject(
new JProperty("id", contentEvent.ContentId), new JProperty("id", contentId),
new JProperty("created", timestamp), new JProperty("created", timestamp),
new JProperty("createdBy", created.Actor.ToString()), new JProperty("createdBy", actor),
new JProperty("lastModified", timestamp), new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", created.Actor.ToString()), new JProperty("lastModifiedBy", actor),
new JProperty("status", Status.Draft.ToString()), new JProperty("status", Status.Draft.ToString()),
new JProperty("data", formatter.ToRouteData(created.Data))); new JProperty("data", formatter.ToRouteData(created.Data)));
break; break;
@ -83,10 +106,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
ruleDescription = $"Update entry in ES index: {action.IndexName}"; ruleDescription = $"Update entry in ES index: {action.IndexName}";
ruleData["Operation"] = "Update"; ruleJob.Operation = "Update";
ruleData["Content"] = new JObject( ruleJob.Content = new JObject(
new JProperty("lastModified", timestamp), new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", updated.Actor.ToString()), new JProperty("lastModifiedBy", actor),
new JProperty("data", formatter.ToRouteData(updated.Data))); new JProperty("data", formatter.ToRouteData(updated.Data)));
break; break;
} }
@ -95,10 +118,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
ruleDescription = $"Update entry in ES index: {action.IndexName}"; ruleDescription = $"Update entry in ES index: {action.IndexName}";
ruleData["Operation"] = "Update"; ruleJob.Operation = "Update";
ruleData["Content"] = new JObject( ruleJob.Content = new JObject(
new JProperty("lastModified", timestamp), new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", statusChanged.Actor.ToString()), new JProperty("lastModifiedBy", actor),
new JProperty("status", statusChanged.Status.ToString())); new JProperty("status", statusChanged.Status.ToString()));
break; break;
} }
@ -107,62 +130,49 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
ruleDescription = $"Delete entry from ES index: {action.IndexName}"; ruleDescription = $"Delete entry from ES index: {action.IndexName}";
ruleData["Operation"] = "Delete"; ruleJob.Operation = "Delete";
ruleData["Content"] = new JObject();
break; 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.")); return (null, new InvalidOperationException("The action cannot handle this event."));
} }
var host = new Uri(job["Host"].Value<string>(), UriKind.Absolute); var client = clients.GetClient((new Uri(job.Host, UriKind.Absolute), job.Username, job.Password));
var username = job["Username"].Value<string>();
var password = job["Password"].Value<string>();
var client = clients.GetClient((host, username, password));
var indexName = job["IndexName"].Value<string>();
var indexType = job["IndexType"].Value<string>();
var operation = operationToken.Value<string>();
var content = job["Content"].Value<JObject>();
var contentId = job["ContentId"].Value<string>();
try try
{ {
switch (operation) switch (job.Operation)
{ {
case "Create": case "Create":
{ {
var doc = content.ToString(); var doc = job.Content.ToString();
var response = await client.IndexAsync<StringResponse>(indexName, indexType, contentId, doc); var response = await client.IndexAsync<StringResponse>(job.IndexName, job.IndexType, job.ContentId, doc);
return (response.Body, response.OriginalException); return (response.Body, response.OriginalException);
} }
case "Update": 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<StringResponse>(indexName, indexType, contentId, doc); var response = await client.UpdateAsync<StringResponse>(job.IndexName, job.IndexType, job.ContentId, doc);
return (response.Body, response.OriginalException); return (response.Body, response.OriginalException);
} }
case "Delete": case "Delete":
{ {
var response = await client.DeleteAsync<StringResponse>(indexName, indexType, contentId); var response = await client.DeleteAsync<StringResponse>(job.IndexName, job.IndexType, job.ContentId);
return (response.Body, response.OriginalException); return (response.Body, response.OriginalException);
} }

65
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs

@ -6,45 +6,55 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; 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.Core.Rules.Actions;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Http; using Squidex.Infrastructure.Http;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
public sealed class FastlyActionHandler : RuleActionHandler<FastlyAction> public sealed class FastlyJob
{ {
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, FastlyAction action) public string FastlyApiKey { get; set; }
{ public string FastlyServiceID { get; set; }
var ruleDescription = "Purge key in fastly";
var ruleData = new RuleJobData public string Key { get; set; }
{ }
["FastlyApiKey"] = action.ApiKey,
["FastlyServiceID"] = action.ServiceId public sealed class FastlyActionHandler : RuleActionHandler<FastlyAction, FastlyJob>
}; {
private const string Description = "Purge key in fastly";
private const string DescriptionIgnore = "Ignore";
protected override Task<(string Description, FastlyJob Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, FastlyAction action)
{
if (@event.Headers.Contains(CommonHeaders.AggregateId)) 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.")); return (null, new InvalidOperationException("The action cannot handle this event."));
} }
var requestMsg = BuildRequest(job, keyToken.Value<string>()); var requestMsg = BuildRequest(job);
HttpResponseMessage response = null; HttpResponseMessage response = null;
@ -59,29 +69,18 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
} }
catch (Exception ex) 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); return (requestDump, ex);
}
else
{
var requestDump = ex.ToString();
return (requestDump, ex);
}
} }
} }
private static HttpRequestMessage BuildRequest(Dictionary<string, JToken> job, string key) private static HttpRequestMessage BuildRequest(FastlyJob job)
{ {
var serviceId = job["FastlyServiceID"].Value<string>(); var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}";
var requestUrl = $"https://api.fastly.com/service/{serviceId}/purge/{key}";
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
request.Headers.Add("Fastly-Key", job["FastlyApiKey"].Value<string>()); request.Headers.Add("Fastly-Key", job.FastlyApiKey);
return request; return request;
} }

70
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs

@ -6,23 +6,41 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Http; using Squidex.Infrastructure.Http;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
public sealed class SlackActionHandler : RuleActionHandler<SlackAction> 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<SlackAction, SlackJob>
{
private const string Description = "Send message to slack";
private readonly RuleEventFormatter formatter; private readonly RuleEventFormatter formatter;
public SlackActionHandler(RuleEventFormatter formatter) public SlackActionHandler(RuleEventFormatter formatter)
@ -32,61 +50,51 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
this.formatter = formatter; this.formatter = formatter;
} }
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, SlackAction action) protected override async Task<(string Description, SlackJob Data)> CreateJobAsync(Envelope<AppEvent> @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 ruleJob = new SlackJob
var ruleData = new RuleJobData
{ {
["RequestUrl"] = action.WebhookUrl, RequestUrl = action.WebhookUrl.ToString(),
["RequestBody"] = body RequestBodyV2 = body.ToString(Formatting.Indented),
}; };
return (ruleDescription, ruleData); return (Description, ruleJob);
} }
private JObject CreatePayload(Envelope<AppEvent> @event, string text) private async Task<JObject> CreatePayloadAsync(Envelope<AppEvent> @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 requestBody = job.Body;
var requestMsg = BuildRequest(job, requestBody); var requestMessage = BuildRequest(job, requestBody);
HttpResponseMessage response = null; HttpResponseMessage response = null;
try try
{ {
response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg); response = await HttpClientPool.GetHttpClient().SendAsync(requestMessage);
var responseString = await response.Content.ReadAsStringAsync(); 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); return (requestDump, null);
} }
catch (Exception ex) catch (Exception ex)
{ {
if (requestMsg != null) var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
{
var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, ex.ToString(), TimeSpan.Zero, false); return (requestDump, ex);
return (requestDump, ex);
}
else
{
throw;
}
} }
} }
private static HttpRequestMessage BuildRequest(Dictionary<string, JToken> job, string requestBody) private static HttpRequestMessage BuildRequest(SlackJob job, string requestBody)
{ {
var requestUrl = job["RequestUrl"].Value<string>(); var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)
{ {
Content = new StringContent(requestBody, Encoding.UTF8, "application/json") Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
}; };

69
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs

@ -6,22 +6,39 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Http; using Squidex.Infrastructure.Http;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
public sealed class WebhookActionHandler : RuleActionHandler<WebhookAction> 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<WebhookAction, WebhookJob>
{ {
private readonly RuleEventFormatter formatter; private readonly RuleEventFormatter formatter;
@ -32,36 +49,34 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
this.formatter = formatter; this.formatter = formatter;
} }
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, WebhookAction action) protected override async Task<(string Description, WebhookJob Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, WebhookAction action)
{ {
var body = formatter.ToRouteData(@event, eventName); var body = formatter.ToRouteData(@event, eventName).ToString(Formatting.Indented);
var signature = $"{body.ToString(Formatting.Indented)}{action.SharedSecret}".Sha256Base64();
var ruleDescription = $"Send event to webhook '{action.Url}'"; var ruleDescription = $"Send event to webhook '{action.Url}'";
var ruleData = new RuleJobData var ruleJob = new WebhookJob
{ {
["RequestUrl"] = action.Url, RequestUrl = await formatter.FormatStringAsync(action.Url.ToString(), @event),
["RequestBody"] = body, RequestSignature = $"{body}{action.SharedSecret}".Sha256Base64(),
["RequestSignature"] = signature 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 requestBody = job.Body;
var requestMsg = BuildRequest(job, requestBody); var requestMessage = BuildRequest(job, requestBody);
HttpResponseMessage response = null; HttpResponseMessage response = null;
try try
{ {
response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg); response = await HttpClientPool.GetHttpClient().SendAsync(requestMessage);
var responseString = await response.Content.ReadAsStringAsync(); 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; Exception ex = null;
@ -74,30 +89,20 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
} }
catch (Exception ex) catch (Exception ex)
{ {
if (requestMsg != null) var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
{
var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
return (requestDump, ex); return (requestDump, ex);
}
else
{
throw;
}
} }
} }
private static HttpRequestMessage BuildRequest(Dictionary<string, JToken> job, string requestBody) private static HttpRequestMessage BuildRequest(WebhookJob job, string requestBody)
{ {
var requestUrl = job["RequestUrl"].Value<string>(); var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
var requestSig = job["RequestSignature"].Value<string>();
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)
{ {
Content = new StringContent(requestBody, Encoding.UTF8, "application/json") 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"); request.Headers.Add("User-Agent", "Squidex Webhook");
return request; return request;

5
src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs

@ -7,6 +7,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -17,8 +18,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{ {
Type ActionType { get; } Type ActionType { get; }
(string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, RuleAction action); Task<(string Description, JObject Data)> CreateJobAsync(Envelope<AppEvent> @event, string eventName, RuleAction action);
Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData data); Task<(string Dump, Exception Exception)> ExecuteJobAsync(JObject data);
} }
} }

22
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs

@ -7,26 +7,36 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules namespace Squidex.Domain.Apps.Core.HandleRules
{ {
public abstract class RuleActionHandler<T> : IRuleActionHandler where T : RuleAction public abstract class RuleActionHandler<TAction, TData> : IRuleActionHandler where TAction : RuleAction
{ {
Type IRuleActionHandler.ActionType Type IRuleActionHandler.ActionType
{ {
get { return typeof(T); } get { return typeof(TAction); }
} }
(string Description, RuleJobData Data) IRuleActionHandler.CreateJob(Envelope<AppEvent> @event, string eventName, RuleAction action) async Task<(string Description, JObject Data)> IRuleActionHandler.CreateJobAsync(Envelope<AppEvent> @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<TData>();
return await ExecuteJobAsync(typedData);
} }
protected abstract (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, T action); protected abstract Task<(string Description, TData Data)> CreateJobAsync(Envelope<AppEvent> @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);
} }
} }

16
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -9,6 +9,7 @@ using System;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -67,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
new JProperty("timestamp", @event.Headers.Timestamp().ToString())); new JProperty("timestamp", @event.Headers.Timestamp().ToString()));
} }
public virtual string FormatString(string text, Envelope<AppEvent> @event) public async virtual Task<string> FormatStringAsync(string text, Envelope<AppEvent> @event)
{ {
var sb = new StringBuilder(text); 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)); sb.Replace(ContentUrlPlaceholder, urlGenerator.GenerateContentUIUrl(@event.Payload.AppId, contentEvent.SchemaId, contentEvent.ContentId));
} }
FormatUserInfo(@event, sb); await FormatUserInfoAsync(@event, sb);
FormatContentAction(@event, sb); FormatContentAction(@event, sb);
var result = sb.ToString(); var result = sb.ToString();
@ -114,7 +116,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return result; return result;
} }
private void FormatUserInfo(Envelope<AppEvent> @event, StringBuilder sb) private async Task FormatUserInfoAsync(Envelope<AppEvent> @event, StringBuilder sb)
{ {
var text = sb.ToString(); var text = sb.ToString();
@ -131,7 +133,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
} }
else else
{ {
var user = FindUser(actor); var user = await FindUserAsync(actor);
if (user != null) if (user != null)
{ {
@ -222,17 +224,17 @@ namespace Squidex.Domain.Apps.Core.HandleRules
}); });
} }
private IUser FindUser(RefToken actor) private Task<IUser> FindUserAsync(RefToken actor)
{ {
var key = $"RuleEventFormatter_Users_${actor.Identifier}"; var key = $"RuleEventFormatter_Users_${actor.Identifier}";
return memoryCache.GetOrCreate(key, x => return memoryCache.GetOrCreateAsync(key, async x =>
{ {
x.AbsoluteExpirationRelativeToNow = UserCacheDuration; x.AbsoluteExpirationRelativeToNow = UserCacheDuration;
try try
{ {
return userResolver.FindByIdOrEmailAsync(actor.Identifier).Result; return await userResolver.FindByIdOrEmailAsync(actor.Identifier);
} }
catch catch
{ {

7
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -11,6 +11,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
this.clock = clock; this.clock = clock;
} }
public virtual RuleJob CreateJob(Rule rule, Envelope<IEvent> @event) public virtual async Task<RuleJob> CreateJobAsync(Rule rule, Envelope<IEvent> @event)
{ {
Guard.NotNull(rule, nameof(rule)); Guard.NotNull(rule, nameof(rule));
Guard.NotNull(@event, nameof(@event)); Guard.NotNull(@event, nameof(@event));
@ -80,7 +81,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
var now = clock.GetCurrentInstant(); var now = clock.GetCurrentInstant();
var actionName = typeNameRegistry.GetName(actionType); var actionName = typeNameRegistry.GetName(actionType);
var actionData = actionHandler.CreateJob(appEventEnvelope, eventName, rule.Action); var actionData = await actionHandler.CreateJobAsync(appEventEnvelope, eventName, rule.Action);
var eventTime = var eventTime =
@event.Headers.Contains(CommonHeaders.Timestamp) ? @event.Headers.Contains(CommonHeaders.Timestamp) ?
@ -113,7 +114,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return job; 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 try
{ {

44
src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs

@ -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<Guid>
{
[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; }
}
}

64
src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs

@ -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<MongoAppEntity>, IAppRepository
{
public MongoAppRepository(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "States_Apps";
}
protected override async Task SetupCollectionAsync(IMongoCollection<MongoAppEntity> collection)
{
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name));
}
public async Task<IReadOnlyList<Guid>> 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<IReadOnlyList<Guid>> 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<Guid> 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;
}
}
}

44
src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository_SnapshotStore.cs

@ -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<AppState, Guid>
{
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));
}
}
}

5
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<AssetState, Guid> public sealed partial class MongoAssetRepository : ISnapshotStore<AssetState, Guid>
{ {
Task ISnapshotStore<AssetState, Guid>.ReadAllAsync(Func<AssetState, long, Task> callback)
{
throw new NotSupportedException();
}
public async Task<(AssetState Value, long Version)> ReadAsync(Guid key) public async Task<(AssetState Value, long Version)> ReadAsync(Guid key)
{ {
var existing = var existing =

5
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -25,11 +25,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonId] [BsonId]
[BsonRequired] [BsonRequired]
[BsonElement]
public string DocumentId { get; set; }
[BsonRequired]
[BsonElement("id")]
[BsonRepresentation(BsonType.String)] [BsonRepresentation(BsonType.String)]
public Guid Id { get; set; } public Guid Id { get; set; }

14
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<ContentState, Guid> public partial class MongoContentRepository : ISnapshotStore<ContentState, Guid>
{ {
Task ISnapshotStore<ContentState, Guid>.ReadAllAsync(Func<ContentState, long, Task> callback)
{
throw new NotSupportedException();
}
public async Task<(ContentState Value, long Version)> ReadAsync(Guid key) public async Task<(ContentState Value, long Version)> ReadAsync(Guid key)
{ {
var contentEntity = var contentEntity =
@ -49,14 +54,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
var idData = value.Data?.ToIdModel(schema.SchemaDef, true); var idData = value.Data?.ToIdModel(schema.SchemaDef, true);
var id = key.ToString();
var document = SimpleMapper.Map(value, new MongoContentEntity var document = SimpleMapper.Map(value, new MongoContentEntity
{ {
AppIdId = value.AppId.Id, AppIdId = value.AppId.Id,
SchemaIdId = value.SchemaId.Id, SchemaIdId = value.SchemaId.Id,
IsDeleted = value.IsDeleted, IsDeleted = value.IsDeleted,
DocumentId = key.ToString(),
DataText = idData?.ToFullText(), DataText = idData?.ToFullText(),
DataByIds = idData, DataByIds = idData,
PendingDataByIds = value.PendingData?.ToIdModel(schema.SchemaDef, true), PendingDataByIds = value.PendingData?.ToIdModel(schema.SchemaDef, true),
@ -67,14 +69,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
try 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) catch (MongoWriteException ex)
{ {
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{ {
var existingVersion = 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(); .FirstOrDefaultAsync();
if (existingVersion != null) if (existingVersion != null)
@ -87,8 +89,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
throw; throw;
} }
} }
document.DocumentId = $"{key}_{newVersion}";
} }
private async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid schemaId) private async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid schemaId)

17
src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs

@ -11,8 +11,6 @@ using NodaTime;
using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History;
using Squidex.Infrastructure; 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 namespace Squidex.Domain.Apps.Entities.MongoDb.History
{ {
internal sealed class ParsedHistoryEvent : IHistoryEventEntity internal sealed class ParsedHistoryEvent : IHistoryEventEntity
@ -25,6 +23,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
get { return inner.Id; } get { return inner.Id; }
} }
public Guid EventId
{
get { return inner.Id; }
}
public Instant Created public Instant Created
{ {
get { return inner.Created; } get { return inner.Created; }
@ -40,11 +43,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
get { return inner.Actor; } get { return inner.Actor; }
} }
public Guid EventId
{
get { return inner.Id; }
}
public long Version public long Version
{ {
get { return inner.Version; } get { return inner.Version; }
@ -55,11 +53,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
get { return inner.Channel; } get { return inner.Channel; }
} }
public string EventType
{
get { return inner.Message; }
}
public string Message public string Message
{ {
get { return message.Value; } get { return message.Value; }

41
src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs

@ -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<Guid>
{
[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; }
}
}

45
src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs

@ -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<MongoRuleEntity>, IRuleRepository
{
public MongoRuleRepository(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "States_Rules";
}
protected override async Task SetupCollectionAsync(IMongoCollection<MongoRuleEntity> collection)
{
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted));
}
public async Task<IReadOnlyList<Guid>> 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();
}
}
}

42
src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs

@ -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<RuleState, Guid>
{
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));
}
}
}

45
src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs

@ -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<Guid>
{
[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; }
}
}

54
src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs

@ -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<MongoSchemaEntity>, ISchemaRepository
{
public MongoSchemaRepository(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "States_Schemas";
}
protected override async Task SetupCollectionAsync(IMongoCollection<MongoSchemaEntity> 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<Guid> 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<IReadOnlyList<Guid>> 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();
}
}
}

43
src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs

@ -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<SchemaState, Guid>
{
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));
}
}
}

31
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -11,11 +11,8 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Entities.Apps; 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;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
@ -26,29 +23,15 @@ namespace Squidex.Domain.Apps.Entities
public sealed class AppProvider : IAppProvider public sealed class AppProvider : IAppProvider
{ {
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
private readonly IAppRepository appRepository;
private readonly IRuleRepository ruleRepository;
private readonly IRequestCache requestCache; private readonly IRequestCache requestCache;
private readonly ISchemaRepository schemaRepository;
public AppProvider(IGrainFactory grainFactory, IRequestCache requestCache)
public AppProvider(
IGrainFactory grainFactory,
IAppRepository appRepository,
ISchemaRepository schemaRepository,
IRuleRepository ruleRepository,
IRequestCache requestCache)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(grainFactory, nameof(grainFactory));
Guard.NotNull(appRepository, nameof(appRepository));
Guard.NotNull(schemaRepository, nameof(schemaRepository));
Guard.NotNull(requestCache, nameof(requestCache)); Guard.NotNull(requestCache, nameof(requestCache));
Guard.NotNull(ruleRepository, nameof(ruleRepository));
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
this.appRepository = appRepository;
this.schemaRepository = schemaRepository;
this.requestCache = requestCache; this.requestCache = requestCache;
this.ruleRepository = ruleRepository;
} }
public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id)
@ -143,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities
{ {
using (Profile.Method<AppProvider>()) using (Profile.Method<AppProvider>())
{ {
var ids = await schemaRepository.QuerySchemaIdsAsync(appId); var ids = await grainFactory.GetGrain<ISchemasByAppIndex>(appId).GetSchemaIdsAsync();
var schemas = var schemas =
await Task.WhenAll( await Task.WhenAll(
@ -160,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities
{ {
using (Profile.Method<AppProvider>()) using (Profile.Method<AppProvider>())
{ {
var ids = await ruleRepository.QueryRuleIdsAsync(appId); var ids = await grainFactory.GetGrain<IRulesByAppIndex>(appId).GetRuleIdsAsync();
var rules = var rules =
await Task.WhenAll( await Task.WhenAll(
@ -177,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities
{ {
using (Profile.Method<AppProvider>()) using (Profile.Method<AppProvider>())
{ {
var ids = await appRepository.QueryUserAppIdsAsync(userId); var ids = await grainFactory.GetGrain<IAppsByUserIndex>(userId).GetAppIdsAsync();
var apps = var apps =
await Task.WhenAll( await Task.WhenAll(
@ -192,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities
{ {
using (Profile.Method<AppProvider>()) using (Profile.Method<AppProvider>())
{ {
return await appRepository.FindAppIdByNameAsync(name); return await grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id).GetAppIdAsync(name);
} }
} }
@ -200,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities
{ {
using (Profile.Method<AppProvider>()) using (Profile.Method<AppProvider>())
{ {
return await schemaRepository.FindSchemaIdAsync(appId, name); return await grainFactory.GetGrain<ISchemasByAppIndex>(appId).GetSchemaIdAsync(name);
} }
} }

2
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -341,7 +341,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
public Task<J<IAppEntity>> GetStateAsync() public Task<J<IAppEntity>> GetStateAsync()
{ {
return Task.FromResult(new J<IAppEntity>(Snapshot)); return J.AsTask<IAppEntity>(Snapshot);
} }
} }
} }

36
src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -20,43 +20,43 @@ namespace Squidex.Domain.Apps.Entities.Apps
: base(typeNameRegistry) : base(typeNameRegistry)
{ {
AddEventMessage<AppContributorAssigned>( AddEventMessage<AppContributorAssigned>(
"assigned {user:[Contributor]} as [Permission]."); "assigned {user:[Contributor]} as {[Permission]}");
AddEventMessage<AppContributorRemoved>( AddEventMessage<AppContributorRemoved>(
"removed {user:[Contributor]}."); "removed {user:[Contributor]} from app");
AddEventMessage<AppClientAttached>( AddEventMessage<AppClientAttached>(
"added client {[Id]}."); "added client {[Id]} to app");
AddEventMessage<AppClientRevoked>( AddEventMessage<AppClientRevoked>(
"revoked client {[Id]}."); "revoked client {[Id]}");
AddEventMessage<AppClientUpdated>( AddEventMessage<AppClientUpdated>(
"updated client {[Id]}."); "updated client {[Id]}");
AddEventMessage<AppClientRenamed>( AddEventMessage<AppClientRenamed>(
"renamed client {[Id]} to {[Name]}."); "renamed client {[Id]} to {[Name]}");
AddEventMessage<AppLanguageAdded>( AddEventMessage<AppLanguageAdded>(
"added language {[Language]}."); "added language {[Language]}");
AddEventMessage<AppLanguageRemoved>( AddEventMessage<AppLanguageRemoved>(
"removed language {[Language]}."); "removed language {[Language]}");
AddEventMessage<AppLanguageUpdated>( AddEventMessage<AppLanguageUpdated>(
"updated language {[Language]}."); "updated language {[Language]}");
AddEventMessage<AppMasterLanguageSet>( AddEventMessage<AppMasterLanguageSet>(
"changed master language to {[Language]}."); "changed master language to {[Language]}");
AddEventMessage<AppPatternAdded>( AddEventMessage<AppPatternAdded>(
"added pattern {[Pattern]}."); "added pattern {[Name]}");
AddEventMessage<AppPatternDeleted>( AddEventMessage<AppPatternDeleted>(
"deleted pattern {[Pattern]}."); "deleted pattern {[Name]}");
AddEventMessage<AppPatternUpdated>( AddEventMessage<AppPatternUpdated>(
"updated pattern {[Pattern]}."); "updated pattern {[Name]}");
} }
protected Task<HistoryEventToStore> On(AppContributorRemoved @event) protected Task<HistoryEventToStore> On(AppContributorRemoved @event)
@ -146,25 +146,25 @@ namespace Squidex.Domain.Apps.Entities.Apps
return Task.FromResult( return Task.FromResult(
ForEvent(@event, channel) ForEvent(@event, channel)
.AddParameter("Pattern", @event.Name)); .AddParameter("Name", @event.Name));
} }
protected Task<HistoryEventToStore> On(AppPatternDeleted @event) protected Task<HistoryEventToStore> On(AppPatternUpdated @event)
{ {
const string channel = "settings.patterns"; const string channel = "settings.patterns";
return Task.FromResult( return Task.FromResult(
ForEvent(@event, channel) ForEvent(@event, channel)
.AddParameter("Pattern", @event.Name)); .AddParameter("Name", @event.Name));
} }
protected Task<HistoryEventToStore> On(AppPatternUpdated @event) protected Task<HistoryEventToStore> On(AppPatternDeleted @event)
{ {
const string channel = "settings.patterns"; const string channel = "settings.patterns";
return Task.FromResult( return Task.FromResult(
ForEvent(@event, channel) ForEvent(@event, channel)
.AddParameter("Pattern", @event.Name)); .AddParameter("Name", @event.Name));
} }
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event) protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)

47
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<IAppsByNameIndex>(SingleGrain.Id);
}
public async Task HandleAsync(CommandContext context, Func<Task> 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();
}
}
}

80
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<string> store;
private IPersistence<State> persistence;
private State state = new State();
[CollectionName("Index_AppsByName")]
public sealed class State
{
public Dictionary<string, Guid> Apps { get; set; } = new Dictionary<string, Guid>();
}
public AppsByNameIndexGrain(IStore<string> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public override Task OnActivateAsync(string key)
{
persistence = store.WithSnapshots<AppsByNameIndexGrain, State, string>(key, s =>
{
state = s;
});
return persistence.ReadAsync();
}
public Task RebuildAsync(Dictionary<string, Guid> 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<Guid> GetAppIdAsync(string appName)
{
state.Apps.TryGetValue(appName, out var appId);
return Task.FromResult(appId);
}
public Task<List<Guid>> GetAppIdsAsync()
{
return Task.FromResult(state.Apps.Values.ToList());
}
}
}

80
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<Task> 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<IAppGrain>(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<EntityCreatedResult<string>>().IdOrValue;
}
private IAppsByUserIndex Index(string id)
{
return grainFactory.GetGrain<IAppsByUserIndex>(id);
}
}
}

73
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<string> store;
private IPersistence<State> persistence;
private State state = new State();
[CollectionName("Index_AppsByUser")]
public sealed class State
{
public HashSet<Guid> Apps { get; set; } = new HashSet<Guid>();
}
public AppsByUserIndexGrain(IStore<string> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public override Task OnActivateAsync(string key)
{
persistence = store.WithSnapshots<AppsByUserIndexGrain, State, string>(key, s =>
{
state = s;
});
return persistence.ReadAsync();
}
public Task RebuildAsync(HashSet<Guid> 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<List<Guid>> GetAppIdsAsync()
{
return Task.FromResult(state.Apps.ToList());
}
}
}

17
src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs → src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs

@ -1,18 +1,27 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; 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<IReadOnlyList<Guid>> QueryRuleIdsAsync(Guid appId); Task AddAppAsync(Guid appId, string name);
Task RemoveAppAsync(Guid appId);
Task RebuildAsync(Dictionary<string, Guid> apps);
Task<Guid> GetAppIdAsync(string name);
Task<List<Guid>> GetAppIdsAsync();
} }
} }

15
src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs → src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs

@ -1,22 +1,25 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; 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<Guid> FindAppIdByNameAsync(string name); Task AddAppAsync(Guid appId);
Task<IReadOnlyList<Guid>> QueryAppIdsAsync(); Task RemoveAppAsync(Guid appId);
Task<IReadOnlyList<Guid>> QueryUserAppIdsAsync(string userId); Task RebuildAsync(HashSet<Guid> apps);
Task<List<Guid>> GetAppIdsAsync();
} }
} }

5
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.Dispatching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Apps.State namespace Squidex.Domain.Apps.Entities.Apps.State
{ {
public class AppState : DomainObjectState<AppState>, [CollectionName("Apps")]
IAppEntity public class AppState : DomainObjectState<AppState>, IAppEntity
{ {
[JsonProperty] [JsonProperty]
public string Name { get; set; } public string Name { get; set; }

2
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public Task<J<IAssetEntity>> GetStateAsync() public Task<J<IAssetEntity>> GetStateAsync()
{ {
return Task.FromResult(new J<IAssetEntity>(Snapshot)); return J.AsTask<IAssetEntity>(Snapshot);
} }
} }
} }

4
src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs

@ -17,9 +17,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Assets.State namespace Squidex.Domain.Apps.Entities.Assets.State
{ {
public class AssetState : DomainObjectState<AssetState>, public class AssetState : DomainObjectState<AssetState>, IAssetEntity, IAssetInfo
IAssetEntity,
IAssetInfo
{ {
[JsonProperty] [JsonProperty]
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }

2
src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs

@ -275,7 +275,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
public Task<J<List<IBackupJob>>> GetStateAsync() public Task<J<List<IBackupJob>>> GetStateAsync()
{ {
return Task.FromResult(new J<List<IBackupJob>>(state.Jobs.OfType<IBackupJob>().ToList())); return J.AsTask(state.Jobs.OfType<IBackupJob>().ToList());
} }
private bool IsRunning() private bool IsRunning()

3
src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs

@ -18,8 +18,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents.State namespace Squidex.Domain.Apps.Entities.Contents.State
{ {
public class ContentState : DomainObjectState<ContentState>, public class ContentState : DomainObjectState<ContentState>, IContentEntity
IContentEntity
{ {
[JsonProperty] [JsonProperty]
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }

15
src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs → src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs

@ -1,20 +1,25 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; 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<Guid> FindSchemaIdAsync(Guid appId, string name); Task AddRuleAsync(Guid ruleId);
Task<IReadOnlyList<Guid>> QuerySchemaIdsAsync(Guid appId); Task RemoveRuleAsync(Guid ruleId);
Task RebuildAsync(HashSet<Guid> rules);
Task<List<Guid>> GetRuleIdsAsync();
} }
} }

56
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<Task> 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<IRuleGrain>(deleteRule.RuleId).GetStateAsync();
await Index(schema.Value.AppId.Id).RemoveRuleAsync(deleteRule.RuleId);
break;
}
}
}
await next();
}
private IRulesByAppIndex Index(Guid appId)
{
return grainFactory.GetGrain<IRulesByAppIndex>(appId);
}
}
}

73
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<Guid> store;
private IPersistence<State> persistence;
private State state = new State();
[CollectionName("Index_RulesByApp")]
public sealed class State
{
public HashSet<Guid> Rules { get; set; } = new HashSet<Guid>();
}
public RulesByAppIndexGrain(IStore<Guid> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public override Task OnActivateAsync(Guid key)
{
persistence = store.WithSnapshots<RulesByAppIndexGrain, State, Guid>(key, s =>
{
state = s;
});
return persistence.ReadAsync();
}
public Task RebuildAsync(HashSet<Guid> 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<List<Guid>> GetRuleIdsAsync()
{
return Task.FromResult(state.Rules.ToList());
}
}
}

2
src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
foreach (var ruleEntity in rules) foreach (var ruleEntity in rules)
{ {
var job = ruleService.CreateJob(ruleEntity.RuleDef, @event); var job = await ruleService.CreateJobAsync(ruleEntity.RuleDef, @event);
if (job != null) if (job != null)
{ {

2
src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -130,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
public Task<J<IRuleEntity>> GetStateAsync() public Task<J<IRuleEntity>> GetStateAsync()
{ {
return Task.FromResult(new J<IRuleEntity>(Snapshot)); return J.AsTask<IRuleEntity>(Snapshot);
} }
} }
} }

5
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;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules.State namespace Squidex.Domain.Apps.Entities.Rules.State
{ {
public class RuleState : DomainObjectState<RuleState>, [CollectionName("Rules")]
IRuleEntity public class RuleState : DomainObjectState<RuleState>, IRuleEntity
{ {
[JsonProperty] [JsonProperty]
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }

27
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<string, Guid> schemas);
Task<Guid> GetSchemaIdAsync(string name);
Task<List<Guid>> GetSchemaIdsAsync();
}
}

56
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<Task> 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<ISchemaGrain>(deleteSchema.SchemaId).GetStateAsync();
await Index(schema.Value.AppId.Id).RemoveSchemaAsync(deleteSchema.SchemaId);
break;
}
}
}
await next();
}
private ISchemasByAppIndex Index(Guid appId)
{
return grainFactory.GetGrain<ISchemasByAppIndex>(appId);
}
}
}

80
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<Guid> store;
private IPersistence<State> persistence;
private State state = new State();
[CollectionName("Index_SchemasByApp")]
public sealed class State
{
public Dictionary<string, Guid> Schemas { get; set; } = new Dictionary<string, Guid>();
}
public SchemasByAppIndexGrain(IStore<Guid> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public override Task OnActivateAsync(Guid key)
{
persistence = store.WithSnapshots<SchemasByAppIndexGrain, State, Guid>(key, s =>
{
state = s;
});
return persistence.ReadAsync();
}
public Task RebuildAsync(Dictionary<string, Guid> 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<Guid> GetSchemaIdAsync(string name)
{
state.Schemas.TryGetValue(name, out var schemaId);
return Task.FromResult(schemaId);
}
public Task<List<Guid>> GetSchemaIdsAsync()
{
return Task.FromResult(state.Schemas.Values.ToList());
}
}
}

2
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -305,7 +305,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public Task<J<ISchemaEntity>> GetStateAsync() public Task<J<ISchemaEntity>> GetStateAsync()
{ {
return Task.FromResult(new J<ISchemaEntity>(Snapshot)); return J.AsTask<ISchemaEntity>(Snapshot);
} }
} }
} }

5
src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs

@ -15,11 +15,12 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas.State namespace Squidex.Domain.Apps.Entities.Schemas.State
{ {
public class SchemaState : DomainObjectState<SchemaState>, [CollectionName("Schemas")]
ISchemaEntity public class SchemaState : DomainObjectState<SchemaState>, ISchemaEntity
{ {
[JsonProperty] [JsonProperty]
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }

13
src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs

@ -5,7 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -26,7 +28,11 @@ namespace Squidex.Infrastructure.States
protected override string CollectionName() protected override string CollectionName()
{ {
return $"States_{typeof(T).Name}"; var attribute = typeof(T).GetCustomAttributes(true).OfType<CollectionNameAttribute>().FirstOrDefault();
var name = attribute?.Name ?? typeof(T).Name;
return $"States_{name}";
} }
public async Task<(T Value, long Version)> ReadAsync(TKey key) 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)); return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.Doc, value));
} }
public Task ReadAllAsync(System.Func<T, long, Task> callback)
{
return Collection.Find(new BsonDocument()).ForEachAsync(x => callback(x.Doc, x.Version));
}
} }
} }

1
src/Squidex.Infrastructure.MongoDb/States/MongoState.cs

@ -11,6 +11,7 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.States namespace Squidex.Infrastructure.States
{ {
[BsonIgnoreExtraElements]
public sealed class MongoState<T, TKey> : IVersionedEntity<TKey> public sealed class MongoState<T, TKey> : IVersionedEntity<TKey>
{ {
[BsonId] [BsonId]

1
src/Squidex.Infrastructure/CachingProviderBase.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure;
namespace Squidex.Infrastructure namespace Squidex.Infrastructure
{ {

4
src/Squidex.Infrastructure/Commands/CommandContext.cs

@ -45,9 +45,11 @@ namespace Squidex.Infrastructure.Commands
this.commandBus = commandBus; this.commandBus = commandBus;
} }
public void Complete(object resultValue = null) public CommandContext Complete(object resultValue = null)
{ {
result = Tuple.Create(resultValue); result = Tuple.Create(resultValue);
return this;
} }
public T Result<T>() public T Result<T>()

3
src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs

@ -7,6 +7,7 @@
using System; using System;
using Orleans; using Orleans;
using Squidex.Infrastructure.Orleans;
namespace Squidex.Infrastructure.EventSourcing.Grains namespace Squidex.Infrastructure.EventSourcing.Grains
{ {
@ -20,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
eventConsumerManagerGrain = new Lazy<IEventConsumerManagerGrain>(() => eventConsumerManagerGrain = new Lazy<IEventConsumerManagerGrain>(() =>
{ {
return factory.GetGrain<IEventConsumerManagerGrain>("Default"); return factory.GetGrain<IEventConsumerManagerGrain>(SingleGrain.Id);
}); });
} }

2
src/Squidex.Infrastructure/Orleans/Bootstrap.cs

@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.Orleans
{ {
try try
{ {
var grain = grainFactory.GetGrain<T>("Default"); var grain = grainFactory.GetGrain<T>(SingleGrain.Id);
await grain.ActivateAsync(); await grain.ActivateAsync();

11
src/Squidex.Infrastructure/Orleans/J.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
#pragma warning disable SA1401 // Fields must be private #pragma warning disable SA1401 // Fields must be private
@ -14,5 +15,15 @@ namespace Squidex.Infrastructure.Orleans
public static class J public static class J
{ {
public static JsonSerializer Serializer = new JsonSerializer(); public static JsonSerializer Serializer = new JsonSerializer();
public static J<T> Of<T>(T value)
{
return value;
}
public static Task<J<T>> AsTask<T>(T value)
{
return Task.FromResult<J<T>>(value);
}
} }
} }

10
src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs → src/Squidex.Infrastructure/Orleans/SingleGrain.cs

@ -1,16 +1,14 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic; namespace Squidex.Infrastructure.Orleans
using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Core.Rules
{ {
public sealed class RuleJobData : Dictionary<string, JToken> public static class SingleGrain
{ {
public const string Id = "Default";
} }
} }

18
src/Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs → src/Squidex.Infrastructure/States/CollectionNameAttribute.cs

@ -5,18 +5,18 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using NodaTime; using System;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
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; } public CollectionNameAttribute(string name)
{
RefToken ScheduledBy { get; } Name = name;
}
} }
} }

2
src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs

@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.States
{ {
public sealed class DefaultStreamNameResolver : IStreamNameResolver 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) public string GetStreamName(Type aggregateType, string id)
{ {

3
src/Squidex.Infrastructure/States/ISnapshotStore.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Squidex.Infrastructure.States namespace Squidex.Infrastructure.States
@ -16,5 +17,7 @@ namespace Squidex.Infrastructure.States
Task<(T Value, long Version)> ReadAsync(TKey key); Task<(T Value, long Version)> ReadAsync(TKey key);
Task ClearAsync(); Task ClearAsync();
Task ReadAllAsync(Func<T, long, Task> callback);
} }
} }

3
src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs

@ -13,6 +13,7 @@ using Orleans;
using Squidex.Areas.Api.Controllers.EventConsumers.Models; using Squidex.Areas.Api.Controllers.EventConsumers.Models;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.EventConsumers namespace Squidex.Areas.Api.Controllers.EventConsumers
@ -28,7 +29,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
public EventConsumersController(ICommandBus commandBus, IGrainFactory grainFactory) public EventConsumersController(ICommandBus commandBus, IGrainFactory grainFactory)
: base(commandBus) : base(commandBus)
{ {
eventConsumerManagerGrain = grainFactory.GetGrain<IEventConsumerManagerGrain>("Default"); eventConsumerManagerGrain = grainFactory.GetGrain<IEventConsumerManagerGrain>(SingleGrain.Id);
} }
[HttpGet] [HttpGet]

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs

@ -46,6 +46,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary> /// </summary>
public bool IsListField { get; set; } public bool IsListField { get; set; }
/// <summary>
/// Optional url to the editor.
/// </summary>
public string EditorUrl { get; set; }
/// <summary> /// <summary>
/// Gets the partitioning of the language, e.g. invariant or language. /// Gets the partitioning of the language, e.g. invariant or language.
/// </summary> /// </summary>

1
src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Users; using Squidex.Shared.Users;

33
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;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; 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.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Backup; 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.History;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands; 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;
using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
@ -116,25 +119,22 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<CreateProfileCommandMiddleware>() services.AddSingletonAs<CreateProfileCommandMiddleware>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<JintScriptEngine>() services.AddSingletonAs<AppsByNameIndexCommandMiddleware>()
.As<IScriptEngine>(); .As<ICommandMiddleware>();
services.AddSingleton<Func<IGrainCallContext, string>>(DomainObjectGrainFormatter.Format);
services.AddTransientAs<AppGrain>() services.AddSingletonAs<AppsByUserIndexCommandMiddleware>()
.AsSelf(); .As<ICommandMiddleware>();
services.AddTransientAs<AssetGrain>() services.AddSingletonAs<RulesByAppIndexCommandMiddleware>()
.AsSelf(); .As<ICommandMiddleware>();
services.AddTransientAs<ContentGrain>() services.AddSingletonAs<SchemasByAppIndexCommandMiddleware>()
.AsSelf(); .As<ICommandMiddleware>();
services.AddTransientAs<RuleGrain>() services.AddSingletonAs<JintScriptEngine>()
.AsSelf(); .As<IScriptEngine>();
services.AddTransientAs<SchemaGrain>() services.AddSingleton<Func<IGrainCallContext, string>>(DomainObjectGrainFormatter.Format);
.AsSelf();
services.AddSingleton(c => services.AddSingleton(c =>
{ {
@ -172,7 +172,10 @@ namespace Squidex.Config.Domain
services.AddTransientAs<ConvertEventStoreAppId>() services.AddTransientAs<ConvertEventStoreAppId>()
.As<IMigration>(); .As<IMigration>();
services.AddTransientAs<DeleteArchiveCollection>() services.AddTransientAs<DeleteArchiveCollectionSetup>()
.As<IMigration>();
services.AddTransientAs<PopulateGrainIndexes>()
.As<IMigration>(); .As<IMigration>();
services.AddTransientAs<RebuildContents>() services.AddTransientAs<RebuildContents>()

74
src/Squidex/Config/Domain/StoreServices.cs

@ -11,34 +11,24 @@ using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Migrate_01.Migrations;
using MongoDB.Driver; using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities; 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.Repositories;
using Squidex.Domain.Apps.Entities.Assets.State; 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.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State; 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.History.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Apps;
using Squidex.Domain.Apps.Entities.MongoDb.Assets; using Squidex.Domain.Apps.Entities.MongoDb.Assets;
using Squidex.Domain.Apps.Entities.MongoDb.Contents; using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.MongoDb.History; using Squidex.Domain.Apps.Entities.MongoDb.History;
using Squidex.Domain.Apps.Entities.MongoDb.Rules; 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.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;
using Squidex.Domain.Users.MongoDb; using Squidex.Domain.Users.MongoDb;
using Squidex.Domain.Users.MongoDb.Infrastructure; using Squidex.Domain.Users.MongoDb.Infrastructure;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
@ -62,63 +52,55 @@ namespace Squidex.Config.Domain
var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName);
var mongoContentDatabase = mongoClient.GetDatabase(mongoContentDatabaseName); var mongoContentDatabase = mongoClient.GetDatabase(mongoContentDatabaseName);
services.AddSingletonAs(c => new MongoXmlRepository(mongoDatabase)) services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>));
.As<IXmlRepository>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoMigrationStatus(mongoDatabase)) services.AddSingletonAs(mongoDatabase)
.As<IMigrationStatus>() .As<IMongoDatabase>();
.As<IInitializable>();
services.AddSingletonAs(c => new MongoSnapshotStore<BackupState, Guid>(mongoDatabase, c.GetRequiredService<JsonSerializer>())) services.AddSingletonAs<MongoXmlRepository>()
.As<ISnapshotStore<BackupState, Guid>>() .As<IXmlRepository>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoSnapshotStore<EventConsumerState, string>(mongoDatabase, c.GetRequiredService<JsonSerializer>())) services.AddSingletonAs<MongoMigrationStatus>()
.As<ISnapshotStore<EventConsumerState, string>>() .As<IMigrationStatus>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoPersistedGrantStore(mongoDatabase)) services.AddSingletonAs<MongoPersistedGrantStore>()
.As<IPersistedGrantStore>() .As<IPersistedGrantStore>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoUsageStore(mongoDatabase)) services.AddSingletonAs<MongoUsageStore>()
.As<IUsageStore>() .As<IUsageStore>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase)) services.AddSingletonAs<MongoRuleEventRepository>()
.As<IRuleEventRepository>() .As<IRuleEventRepository>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) services.AddSingletonAs<MongoUserStore>()
.As<IUserStore<IUser>>() .As<IUserStore<IUser>>()
.As<IUserFactory>() .As<IUserFactory>()
.As<IUserResolver>() .As<IUserResolver>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoRoleStore(mongoDatabase)) services.AddSingletonAs<MongoRoleStore>()
.As<IRoleStore<IRole>>() .As<IRoleStore<IRole>>()
.As<IRoleFactory>() .As<IRoleFactory>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase)) services.AddSingletonAs<MongoHistoryEventRepository>()
.As<IAppRepository>() .As<IHistoryEventRepository>()
.As<ISnapshotStore<AppState, Guid>>() .As<IEventConsumer>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase))
.As<IAssetRepository>()
.As<ISnapshotStore<AssetState, Guid>>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoRuleRepository(mongoDatabase)) services.AddSingletonAs<MongoAssetStatsRepository>()
.As<IRuleRepository>() .As<IAssetStatsRepository>()
.As<ISnapshotStore<RuleState, Guid>>() .As<IEventConsumer>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase)) services.AddSingletonAs<MongoAssetRepository>()
.As<ISchemaRepository>() .As<IAssetRepository>()
.As<ISnapshotStore<SchemaState, Guid>>() .As<ISnapshotStore<AssetState, Guid>>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService<IAppProvider>())) services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService<IAppProvider>()))
@ -127,15 +109,11 @@ namespace Squidex.Config.Domain
.As<IEventConsumer>() .As<IEventConsumer>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices<IHistoryEventsCreator>())) services.AddTransientAs(c => new DeleteArchiveCollectionSetup(mongoContentDatabase))
.As<IHistoryEventRepository>() .As<IMigration>();
.As<IEventConsumer>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoAssetStatsRepository(mongoDatabase)) services.AddTransientAs<ConvertOldSnapshotStores>()
.As<IAssetStatsRepository>() .As<IMigration>();
.As<IEventConsumer>()
.As<IInitializable>();
} }
}); });

3
src/Squidex/Config/Orleans/SiloWrapper.cs

@ -7,7 +7,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -57,7 +56,7 @@ namespace Squidex.Config.Orleans
J.Serializer = SerializationServices.DefaultJsonSerializer; J.Serializer = SerializationServices.DefaultJsonSerializer;
var hostBuilder = new SiloHostBuilder() var hostBuilder = new SiloHostBuilder()
.UseDashboard(options => options.HostSelf = true) .UseDashboard(options => options.HostSelf = false)
.AddStartupTask<Bootstrap<IContentSchedulerGrain>>() .AddStartupTask<Bootstrap<IContentSchedulerGrain>>()
.AddStartupTask<Bootstrap<IEventConsumerManagerGrain>>() .AddStartupTask<Bootstrap<IEventConsumerManagerGrain>>()
.AddStartupTask<Bootstrap<IRuleDequeuerGrain>>() .AddStartupTask<Bootstrap<IRuleDequeuerGrain>>()

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

@ -79,7 +79,7 @@ namespace Squidex.Pipeline.Swagger
public static SwaggerSecurityScheme CreateOAuthSchema(MyUrlsOptions urlOptions) 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 securityDocs = LoadDocs("security");
var securityText = securityDocs.Replace("<TOKEN_URL>", tokenUrl); var securityText = securityDocs.Replace("<TOKEN_URL>", tokenUrl);

1
src/Squidex/Program.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;

194
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -21,99 +21,107 @@
<sqx-control-errors [for]="selectedFormControl" [fieldName]="field.displayName" [submitted]="contentFormSubmitted"></sqx-control-errors> <sqx-control-errors [for]="selectedFormControl" [fieldName]="field.displayName" [submitted]="contentFormSubmitted"></sqx-control-errors>
<div [ngSwitch]="field.properties.fieldType"> <div>
<div *ngSwitchCase="'Number'"> <ng-container *ngIf="field.properties.editorUrl">
<div [ngSwitch]="field.properties['editor']"> <sqx-iframe-editor [url]="field.properties.editorUrl" [formControl]="selectedFormControl"></sqx-iframe-editor>
<div *ngSwitchCase="'Input'"> </ng-container>
<input class="form-control" type="number" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div> <ng-container *ngIf="!field.properties.editorUrl">
<div *ngSwitchCase="'Stars'"> <ng-container [ngSwitch]="field.properties.fieldType">
<sqx-stars [formControl]="selectedFormControl" [maximumStars]="field.properties['maxValue']"></sqx-stars> <ng-container *ngSwitchCase="'Number'">
</div> <ng-container [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Dropdown'"> <ng-container *ngSwitchCase="'Input'">
<select class="form-control" [formControl]="selectedFormControl"> <input class="form-control" type="number" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
<option [ngValue]="null"></option> </ng-container>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option> <ng-container *ngSwitchCase="'Stars'">
</select> <sqx-stars [formControl]="selectedFormControl" [maximumStars]="field.properties['maxValue']"></sqx-stars>
</div> </ng-container>
<div *ngSwitchCase="'Radio'"> <ng-container *ngSwitchCase="'Dropdown'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']"> <select class="form-control" [formControl]="selectedFormControl">
<input class="form-check-input" type="radio" [value]="value" [formControl]="selectedFormControl" /> <option [ngValue]="null"></option>
<label class="form-check-label"> <option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
{{value}} </select>
</label> </ng-container>
</div> <ng-container *ngSwitchCase="'Radio'">
</div> <ng-container class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
</div> <input class="form-check-input" type="radio" [value]="value" [formControl]="selectedFormControl" />
</div> <label class="form-check-label">
<div *ngSwitchCase="'String'"> {{value}}
<div [ngSwitch]="field.properties['editor']"> </label>
<div *ngSwitchCase="'Input'"> </ng-container>
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" /> </ng-container>
</div> </ng-container>
<div *ngSwitchCase="'Slug'"> </ng-container>
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" sqxSlugifyInput /> <ng-container *ngSwitchCase="'String'">
</div> <ng-container [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'TextArea'"> <ng-container *ngSwitchCase="'Input'">
<textarea class="form-control" [formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea> <input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div> </ng-container>
<div *ngSwitchCase="'RichText'"> <ng-container *ngSwitchCase="'Slug'">
<sqx-rich-editor [formControl]="selectedFormControl"></sqx-rich-editor> <input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify" />
</div> </ng-container>
<div *ngSwitchCase="'Markdown'"> <ng-container *ngSwitchCase="'TextArea'">
<sqx-markdown-editor [formControl]="selectedFormControl"></sqx-markdown-editor> <textarea class="form-control" [formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</div> </ng-container>
<div *ngSwitchCase="'Dropdown'"> <ng-container *ngSwitchCase="'RichText'">
<select class="form-control" [formControl]="selectedFormControl"> <sqx-rich-editor [formControl]="selectedFormControl"></sqx-rich-editor>
<option [ngValue]="null"></option> </ng-container>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option> <ng-container *ngSwitchCase="'Markdown'">
</select> <sqx-markdown-editor [formControl]="selectedFormControl"></sqx-markdown-editor>
</div> </ng-container>
<div *ngSwitchCase="'Radio'"> <ng-container *ngSwitchCase="'Dropdown'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']"> <select class="form-control" [formControl]="selectedFormControl">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="selectedFormControl" /> <option [ngValue]="null"></option>
<label class="form-check-label"> <option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
{{value}} </select>
</label> </ng-container>
</div> <ng-container *ngSwitchCase="'Radio'">
</div> <ng-container class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
</div> <input class="form-check-input" type="radio" value="{{value}}" [formControl]="selectedFormControl" />
</div> <label class="form-check-label">
<div *ngSwitchCase="'Boolean'"> {{value}}
<div [ngSwitch]="field.properties['editor']"> </label>
<div *ngSwitchCase="'Toggle'"> </ng-container>
<sqx-toggle [formControl]="selectedFormControl"></sqx-toggle> </ng-container>
</div> </ng-container>
<div *ngSwitchCase="'Checkbox'"> </ng-container>
<div class="form-check form-check-inline"> <ng-container *ngSwitchCase="'Boolean'">
<input class="form-check-input" type="checkbox" [formControl]="selectedFormControl" sqxIndeterminateValue /> <ng-container [ngSwitch]="field.properties['editor']">
</div> <ng-container *ngSwitchCase="'Toggle'">
</div> <sqx-toggle [formControl]="selectedFormControl"></sqx-toggle>
</div> </ng-container>
</div> <ng-container *ngSwitchCase="'Checkbox'">
<div *ngSwitchCase="'DateTime'"> <ng-container class="form-check form-check-inline">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties['editor']" [formControl]="selectedFormControl"></sqx-date-time-editor> <input class="form-check-input" type="checkbox" [formControl]="selectedFormControl" sqxIndeterminateValue />
</div> </ng-container>
<div *ngSwitchCase="'Geolocation'"> </ng-container>
<sqx-geolocation-editor [formControl]="selectedFormControl"></sqx-geolocation-editor> </ng-container>
</div> </ng-container>
<div *ngSwitchCase="'Json'"> <ng-container *ngSwitchCase="'DateTime'">
<sqx-json-editor [formControl]="selectedFormControl"></sqx-json-editor> <sqx-date-time-editor enforceTime="true" [mode]="field.properties['editor']" [formControl]="selectedFormControl"></sqx-date-time-editor>
</div> </ng-container>
<div *ngSwitchCase="'Assets'"> <ng-container *ngSwitchCase="'Geolocation'">
<sqx-assets-editor [formControl]="selectedFormControl"></sqx-assets-editor> <sqx-geolocation-editor [formControl]="selectedFormControl"></sqx-geolocation-editor>
</div> </ng-container>
<div *ngSwitchCase="'Tags'"> <ng-container *ngSwitchCase="'Json'">
<sqx-tag-editor [formControl]="selectedFormControl"></sqx-tag-editor> <sqx-json-editor [formControl]="selectedFormControl"></sqx-json-editor>
</div> </ng-container>
<div *ngSwitchCase="'References'"> <ng-container *ngSwitchCase="'Assets'">
<sqx-references-editor <sqx-assets-editor [formControl]="selectedFormControl"></sqx-assets-editor>
[formControl]="selectedFormControl" </ng-container>
[language]="language" <ng-container *ngSwitchCase="'Tags'">
[languages]="languages" <sqx-tag-editor [formControl]="selectedFormControl"></sqx-tag-editor>
[schemaId]="field.properties['schemaId']"> </ng-container>
</sqx-references-editor> <ng-container *ngSwitchCase="'References'">
</div> <sqx-references-editor
[formControl]="selectedFormControl"
[language]="language"
[languages]="languages"
[schemaId]="field.properties['schemaId']">
</sqx-references-editor>
</ng-container>
</ng-container>
</ng-container>
</div> </div>
<ng-container *ngIf="field.properties['hints']; let hints"> <ng-container *ngIf="field.properties['hints']; let hints">

32
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -48,20 +48,24 @@ export class AssetsEditorComponent implements ControlValueAccessor {
) { ) {
} }
public writeValue(value: string[]) { public writeValue(obj: any) {
if (Types.isArrayOfString(value) && !Types.isEquals(value, this.oldAssets.map(x => x.id).values)) { if (Types.isArrayOfString(obj)) {
const assetIds: string[] = value; if (!Types.isEquals(obj, this.oldAssets.map(x => x.id).values)) {
const assetIds: string[] = obj;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, value)
.subscribe(dtos => { this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, obj)
this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!)); .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(); if (this.oldAssets.length !== assetIds.length) {
} this.updateValue();
}, () => { }
this.oldAssets = ImmutableArray.empty<AssetDto>(); }, () => {
}); this.oldAssets = ImmutableArray.empty();
});
}
} else {
this.oldAssets = ImmutableArray.empty();
} }
} }

2
src/Squidex/app/features/content/shared/content-item.component.html

@ -27,7 +27,7 @@
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" /> <input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div> </div>
<div *ngSwitchCase="'Slug'"> <div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxSlugifyInput /> <input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify" />
</div> </div>
<div *ngSwitchCase="'Dropdown'"> <div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name"> <select class="form-control" [formControlName]="field.name">

32
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -77,20 +77,24 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
}); });
} }
public writeValue(value: string[]) { public writeValue(obj: any) {
if (Types.isArrayOfString(value) && !Types.isEquals(value, this.contentItems.map(x => x.id).values)) { if (Types.isArrayOfString(obj)) {
const contentIds: string[] = value; if (!Types.isEquals(obj, this.contentItems.map(x => x.id).values)) {
const contentIds: string[] = obj;
this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds)
.subscribe(dtos => { this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds)
this.contentItems = ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)).filter(r => !!r).map(r => r!)); .subscribe(dtos => {
this.contentItems = ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)).filter(r => !!r).map(r => r!));
if (this.contentItems.length !== contentIds.length) {
this.updateValue(); if (this.contentItems.length !== contentIds.length) {
} this.updateValue();
}, () => { }
this.contentItems = ImmutableArray.empty<ContentDto>(); }, () => {
}); this.contentItems = ImmutableArray.empty();
});
}
} else {
this.contentItems = ImmutableArray.empty();
} }
} }

2
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html

@ -19,7 +19,7 @@
<i class="icon-type-{{fieldType.type}}"></i> <i class="icon-type-{{fieldType.type}}"></i>
</div> </div>
</div> </div>
<div class="col"> <div class="col-lg">
<div class="type-title">{{fieldType.type}}</div> <div class="type-title">{{fieldType.type}}</div>
<div class="type-text text-muted">{{fieldType.description}}</div> <div class="type-text text-muted">{{fieldType.description}}</div>
</div> </div>

7
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts

@ -12,7 +12,8 @@ import {
AddFieldForm, AddFieldForm,
fieldTypes, fieldTypes,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState SchemasState,
Types
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -53,7 +54,9 @@ export class FieldWizardComponent {
this.addFieldForm.submitCompleted({ type: fieldTypes[0].type }); this.addFieldForm.submitCompleted({ type: fieldTypes[0].type });
if (next) { if (next) {
this.nameInput.nativeElement.focus(); if (Types.isFunction(this.nameInput.nativeElement.focus)) {
this.nameInput.nativeElement.focus();
}
} else { } else {
this.complete(); this.complete();
} }

2
src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss

@ -44,7 +44,7 @@
& { & {
@include circle(5.25rem); @include circle(5.25rem);
@include box-shadow(0, 8px, 16px, .3); @include box-shadow(0, 8px, 16px, .3);
@include absolute(auto, 2rem, 1rem, auto); @include absolute(auto, 5rem, 1rem, auto);
} }
&-icon { &-icon {

11
src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html

@ -1,4 +1,15 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_editorUrl">Editor Url</label>
<div class="col col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_editorUrl" formControlName="editorUrl" />
<small class="form-text text-muted">
Url to your plugin if you use a custom editor.
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label> <label class="col col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label>

5
src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts

@ -31,11 +31,6 @@ export class BooleanUIComponent implements OnInit {
Validators.required Validators.required
])); ]));
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.setControl('inlineEditable', this.editForm.setControl('inlineEditable',
new FormControl(this.properties.inlineEditable)); new FormControl(this.properties.inlineEditable));
} }

11
src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html

@ -1,4 +1,15 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_editorUrl">Editor Url</label>
<div class="col col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_editorUrl" formControlName="editorUrl" />
<small class="form-text text-muted">
Url to your plugin if you use a custom editor.
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label> <label class="col col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label>

5
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, [ new FormControl(this.properties.editor, [
Validators.required Validators.required
])); ]));
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
} }
} }

11
src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html

@ -1,4 +1,15 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_editorUrl">Editor Url</label>
<div class="col col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_editorUrl" formControlName="editorUrl" />
<small class="form-text text-muted">
Url to your plugin if you use a custom editor.
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col col-3 col-form-label">Editor</label> <label class="col col-3 col-form-label">Editor</label>

12
src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html

@ -1,3 +1,13 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<span>Nothing to setup</span> <div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_editorUrl">Editor Url</label>
<div class="col col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_editorUrl" formControlName="editorUrl" />
<small class="form-text text-muted">
Url to your plugin if you use a custom editor.
</small>
</div>
</div>
<div> <div>

6
src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss

@ -1,2 +1,6 @@
@import '_vars'; @import '_vars';
@import '_mixins'; @import '_mixins';
.form-group {
margin: 0;
}

11
src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html

@ -1,4 +1,15 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_editorUrl">Editor Url</label>
<div class="col col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_editorUrl" formControlName="editorUrl" />
<small class="form-text text-muted">
Url to your plugin if you use a custom editor.
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label> <label class="col col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label>

5
src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts

@ -45,11 +45,6 @@ export class NumberUIComponent implements OnDestroy, OnInit {
Validators.required Validators.required
])); ]));
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.setControl('allowedValues', this.editForm.setControl('allowedValues',
new FormControl(this.properties.allowedValues, [])); new FormControl(this.properties.allowedValues, []));

11
src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html

@ -1,4 +1,15 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_editorUrl">Editor Url</label>
<div class="col col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_editorUrl" formControlName="editorUrl" />
<small class="form-text text-muted">
Url to your plugin if you use a custom editor.
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label> <label class="col col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label>

5
src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts

@ -43,11 +43,6 @@ export class StringUIComponent implements OnDestroy, OnInit {
Validators.required Validators.required
])); ]));
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.setControl('allowedValues', this.editForm.setControl('allowedValues',
new FormControl(this.properties.allowedValues)); new FormControl(this.properties.allowedValues));

2
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html

@ -17,7 +17,7 @@
<sqx-control-errors for="name" submitOnly="true" [submitted]="createForm.submitted | async"></sqx-control-errors> <sqx-control-errors for="name" submitOnly="true" [submitted]="createForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="schemaName" formControlName="name" autocomplete="off" sqxLowerCaseInput sqxFocusOnInit /> <input type="text" class="form-control" id="schemaName" formControlName="name" autocomplete="off" sqxTransformInput="LowerCase" sqxFocusOnInit />
<small class="form-text text-muted"> <small class="form-text text-muted">
The schema name becomes part of the api url,<br /> e.g {{apiUrl.buildUrl("api/content/")}}{{appsState.appName}}/<b>{{createForm.schemaName | async}}</b>/. The schema name becomes part of the api url,<br /> e.g {{apiUrl.buildUrl("api/content/")}}{{appsState.appName}}/<b>{{createForm.schemaName | async}}</b>/.

2
src/Squidex/app/features/settings/pages/clients/clients-page.component.html

@ -27,7 +27,7 @@
<div class="col"> <div class="col">
<sqx-control-errors for="name" [submitted]="addClientForm.submitted | async"></sqx-control-errors> <sqx-control-errors for="name" [submitted]="addClientForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter client name" autocomplete="off" sqxLowerCaseInput /> <input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter client name" autocomplete="off" sqxTransformInput="LowerCase" />
</div> </div>
<div class="col col-auto pl-1"> <div class="col col-auto pl-1">
<button type="submit" class="btn btn-success" [disabled]="addClientForm.hasNoName | async">Add Client</button> <button type="submit" class="btn btn-success" [disabled]="addClientForm.hasNoName | async">Add Client</button>

3
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -15,6 +15,7 @@ import {
AssignContributorForm, AssignContributorForm,
AutocompleteSource, AutocompleteSource,
ContributorsState, ContributorsState,
Types,
UserDto, UserDto,
UsersService UsersService
} from '@app/shared'; } from '@app/shared';
@ -85,7 +86,7 @@ export class ContributorsPageComponent implements OnInit {
if (value) { if (value) {
let user = value.user; let user = value.user;
if (user instanceof UserDto) { if (Types.is(user, UserDto)) {
user = user.id; user = user.id;
} }

8
src/Squidex/app/framework/angular/forms/autocomplete.component.ts

@ -104,14 +104,14 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
return true; return true;
} }
public writeValue(value: any) { public writeValue(obj: any) {
if (!value) { if (!obj) {
this.resetForm(); this.resetForm();
} else { } else {
const item = this.items.find(i => i === value); const item = this.items.find(i => i === obj);
if (item) { if (item) {
this.queryInput.setValue(value.title || ''); this.queryInput.setValue(obj.title || '');
} }
} }

6
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 { AbstractControl, FormGroupDirective } from '@angular/forms';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { fadeAnimation } from '@app/framework/internal'; import { fadeAnimation, Types } from '@app/framework/internal';
const DEFAULT_ERRORS: { [key: string]: string } = { const DEFAULT_ERRORS: { [key: string]: string } = {
required: '{field} is required.', required: '{field} is required.',
@ -71,7 +71,7 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy {
if (this.fieldName) { if (this.fieldName) {
this.displayFieldName = this.fieldName; this.displayFieldName = this.fieldName;
} else if (this.for) { } 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); this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1);
} else { } else {
this.displayFieldName = 'field'; this.displayFieldName = 'field';
@ -80,7 +80,7 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy {
let control: AbstractControl | null = null; let control: AbstractControl | null = null;
if (typeof this.for === 'string') { if (Types.isString(this.for)) {
control = this.formGroupDirective.form.controls[this.for]; control = this.formGroupDirective.form.controls[this.for];
} else { } else {
control = this.for; control = this.for;

4
src/Squidex/app/framework/angular/forms/copy.directive.ts

@ -40,7 +40,7 @@ export class CopyDirective {
element.focus(); element.focus();
if (element instanceof HTMLInputElement) { if (Types.is(element, HTMLInputElement)) {
element.setSelectionRange(0, element.value.length); element.setSelectionRange(0, element.value.length);
} }
@ -56,7 +56,7 @@ export class CopyDirective {
currentFocus.focus(); currentFocus.focus();
} }
if (element instanceof HTMLInputElement) { if (Types.is(element, HTMLInputElement)) {
element.setSelectionRange(prevSelectionStart!, prevSelectionEnd!); element.setSelectionRange(prevSelectionStart!, prevSelectionEnd!);
} }
} }

12
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) { public writeValue(obj: any) {
if (!Types.isString(value) || value.length === 0) { if (Types.isString(obj) && obj.length > 0) {
this.timeValue = null; const parsed = moment.parseZone(obj);
this.dateValue = null;
} else {
const parsed = moment.parseZone(value);
this.dateValue = moment(parsed); this.dateValue = moment(parsed);
if (this.showTime) { if (this.showTime) {
this.timeValue = moment(parsed); this.timeValue = moment(parsed);
} }
} else {
this.timeValue = null;
this.dateValue = null;
} }
this.updateControls(); this.updateControls();

4
src/Squidex/app/framework/angular/forms/dropdown.component.ts

@ -59,8 +59,8 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
} }
} }
public writeValue(value: any) { public writeValue(obj: any) {
this.selectIndex(this.items && value ? this.items.indexOf(value) : 0); this.selectIndex(this.items && obj ? this.items.indexOf(obj) : 0);
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {

8
src/Squidex/app/framework/angular/forms/file-drop.directive.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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({ @Directive({
selector: '[sqxFileDrop]' selector: '[sqxFileDrop]'
@ -18,7 +18,7 @@ export class FileDropDirective {
constructor( constructor(
private readonly element: ElementRef, private readonly element: ElementRef,
private readonly renderer: Renderer private readonly renderer: Renderer2
) { ) {
} }
@ -71,7 +71,7 @@ export class FileDropDirective {
this.dragCounter++; this.dragCounter++;
if (this.dragCounter === 1) { 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; this.dragCounter = number || this.dragCounter - 1;
if (this.dragCounter === 0) { if (this.dragCounter === 0) {
this.renderer.setElementClass(this.element.nativeElement, 'drag', false); this.renderer.removeClass(this.element.nativeElement, 'drag');
} }
} }

48
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. * 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'; import { FocusOnInitDirective } from './focus-on-init.directive';
describe('FocusOnInitDirective', () => { 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) => { it('should call focus on element when init', (cb) => {
const calledMethods: string[] = []; const directive = new FocusOnInitDirective(element);
const calledElements: any[] = []; directive.select = false;
directive.ngAfterViewInit();
const renderer = { setTimeout(() => {
invokeElementMethod: (elem: any, method: any) => { expect(isFocusCalled).toBeTruthy();
calledElements.push(elem); expect(isSelectCalled).toBeFalsy();
calledMethods.push(method);
}
};
const element: ElementRef = { cb();
nativeElement: {} }, 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.select = true;
directive.ngAfterViewInit(); directive.ngAfterViewInit();
setTimeout(() => { setTimeout(() => {
expect(calledMethods).toEqual(['focus', 'select']); expect(isFocusCalled).toBeTruthy();
expect(calledElements).toEqual([element.nativeElement, element.nativeElement]); expect(isSelectCalled).toBeTruthy();
cb(); cb();
}, 200); }, 200);

15
src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts

@ -5,7 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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({ @Directive({
selector: '[sqxFocusOnInit]' selector: '[sqxFocusOnInit]'
@ -15,17 +17,20 @@ export class FocusOnInitDirective implements AfterViewInit {
public select: boolean; public select: boolean;
constructor( constructor(
private readonly element: ElementRef, private readonly element: ElementRef
private readonly renderer: Renderer
) { ) {
} }
public ngAfterViewInit() { public ngAfterViewInit() {
setTimeout(() => { setTimeout(() => {
this.renderer.invokeElementMethod(this.element.nativeElement, 'focus', []); if (Types.isFunction(this.element.nativeElement.focus)) {
this.element.nativeElement.focus();
}
if (this.select) { if (this.select) {
this.renderer.invokeElementMethod(this.element.nativeElement, 'select', []); if (Types.isFunction(this.element.nativeElement.select)) {
this.element.nativeElement.select();
}
} }
}, 100); }, 100);
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save