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
## v1.4.1 - 2018-05-02
### Bugfixes
* **Orleans**: Remove orleans dashboard from 8080.
## v1.4.0 - 2018-05-02
### Features
* **UI**: Big refactorings and UI improvements.
* **Actions**: New log formatter with placeholder for user infos.
* **Actions**: Azure Queue action.
* **Actions**: Algolia action.
* **Actions**: Fastly action.
* **Backup**: Backup all your data to an archive.
## v1.3.0 - 2018-02-17
### Features
* **Actions**: ElasticSearch action
* **Actions**: ElasticSearch action.
### Refactorings

2
Dockerfile

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

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

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

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 EditorUrl { get; set; }
public abstract T Accept<T>(IFieldPropertiesVisitor<T> visitor);
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.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class AlgoliaActionHandler : RuleActionHandler<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 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)
{
ruleData["ContentId"] = contentEvent.ContentId.ToString();
ruleData["Operation"] = "Upsert";
ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event);
var contentId = contentEvent.ContentId.ToString();
var ruleDescription = string.Empty;
var ruleJob = new AlgoliaJob
{
AppId = action.AppId,
ApiKey = action.ApiKey,
ContentId = contentId,
IndexName = await formatter.FormatStringAsync(action.IndexName, @event)
};
var timestamp = @event.Headers.Timestamp().ToString();
@ -62,8 +77,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Add entry to Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject(
new JProperty("id", contentEvent.ContentId),
ruleJob.Content = new JObject(
new JProperty("objectID", contentId),
new JProperty("id", contentId),
new JProperty("created", timestamp),
new JProperty("createdBy", created.Actor.ToString()),
new JProperty("lastModified", timestamp),
@ -77,7 +93,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject(
ruleJob.Content = new JObject(
new JProperty("objectID", contentId),
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", updated.Actor.ToString()),
new JProperty("data", formatter.ToRouteData(updated.Data)));
@ -88,7 +105,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject(
ruleJob.Content = new JObject(
new JProperty("objectID", contentId),
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", statusChanged.Actor.ToString()),
new JProperty("status", statusChanged.Status.ToString()));
@ -99,55 +117,38 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleDescription = $"Delete entry from Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject();
ruleData["Operation"] = "Delete";
break;
}
}
return (ruleDescription, ruleJob);
}
return (ruleDescription, ruleData);
return (DescriptionIgnore, new AlgoliaJob());
}
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(AlgoliaJob job)
{
if (!job.TryGetValue("Operation", out var operationToken))
if (string.IsNullOrWhiteSpace(job.AppId))
{
return (null, new InvalidOperationException("The action cannot handle this event."));
}
var appId = job["AppId"].Value<string>();
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>();
var index = clients.GetClient((job.AppId, job.ApiKey, job.IndexName));
try
{
switch (operation)
if (job.Content != null)
{
case "Upsert":
{
content["objectID"] = contentId;
var response = await index.PartialUpdateObjectAsync(content);
return (response.ToString(Formatting.Indented), null);
}
case "Delete":
{
var response = await index.DeleteObjectAsync(contentId);
var response = await index.PartialUpdateObjectAsync(job.Content);
return (response.ToString(Formatting.Indented), null);
}
return (response.ToString(Formatting.Indented), null);
}
else
{
var response = await index.DeleteObjectAsync(job.ContentId);
default:
return (null, null);
return (response.ToString(Formatting.Indented), null);
}
}
catch (AlgoliaException ex)

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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
#pragma warning disable SA1649 // File name must match first type name
namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class AzureQueueActionHandler : RuleActionHandler<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 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 ruleData = new RuleJobData
var queueName = await formatter.FormatStringAsync(action.Queue, @event);
var ruleDescription = $"Send AzureQueueJob to azure queue '{action.Queue}'";
var ruleJob = new AzureQueueJob
{
["QueueConnectionString"] = action.ConnectionString,
["QueueName"] = action.Queue,
["MessageBody"] = body
QueueConnectionString = action.ConnectionString,
QueueName = queueName,
MessageBodyV2 = body,
};
return (ruleDescription, ruleData);
return (ruleDescription, ruleJob);
}
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(AzureQueueJob job)
{
var queueConnectionString = job["QueueConnectionString"].Value<string>();
var queueName = job["QueueName"].Value<string>();
var queue = clients.GetClient((queueConnectionString, queueName));
var messageBody = job["MessageBody"].ToString(Formatting.Indented);
var queue = clients.GetClient((job.QueueConnectionString, job.QueueName));
await queue.AddMessageAsync(new CloudQueueMessage(messageBody));
await queue.AddMessageAsync(new CloudQueueMessage(job.Body));
return ("Completed", null);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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>
{
Task ISnapshotStore<ContentState, Guid>.ReadAllAsync(Func<ContentState, long, Task> callback)
{
throw new NotSupportedException();
}
public async Task<(ContentState Value, long Version)> ReadAsync(Guid key)
{
var contentEntity =
@ -49,14 +54,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
var idData = value.Data?.ToIdModel(schema.SchemaDef, true);
var id = key.ToString();
var document = SimpleMapper.Map(value, new MongoContentEntity
{
AppIdId = value.AppId.Id,
SchemaIdId = value.SchemaId.Id,
IsDeleted = value.IsDeleted,
DocumentId = key.ToString(),
DataText = idData?.ToFullText(),
DataByIds = idData,
PendingDataByIds = value.PendingData?.ToIdModel(schema.SchemaDef, true),
@ -67,14 +69,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
try
{
await Collection.ReplaceOneAsync(x => x.DocumentId == id && x.Version == oldVersion, document, Upsert);
await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, document, Upsert);
}
catch (MongoWriteException ex)
{
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
var existingVersion =
await Collection.Find(x => x.DocumentId == id).Only(x => x.DocumentId, x => x.Version)
await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version)
.FirstOrDefaultAsync();
if (existingVersion != null)
@ -87,8 +89,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
throw;
}
}
document.DocumentId = $"{key}_{newVersion}";
}
private async Task<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.Infrastructure;
#pragma warning disable RECS0029 // Warns about property or indexer setters and event adders or removers that do not use the value parameter
namespace Squidex.Domain.Apps.Entities.MongoDb.History
{
internal sealed class ParsedHistoryEvent : IHistoryEventEntity
@ -25,6 +23,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
get { return inner.Id; }
}
public Guid EventId
{
get { return inner.Id; }
}
public Instant Created
{
get { return inner.Created; }
@ -40,11 +43,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
get { return inner.Actor; }
}
public Guid EventId
{
get { return inner.Id; }
}
public long Version
{
get { return inner.Version; }
@ -55,11 +53,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
get { return inner.Channel; }
}
public string EventType
{
get { return inner.Message; }
}
public string Message
{
get { return message.Value; }

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 Orleans;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log;
@ -26,29 +23,15 @@ namespace Squidex.Domain.Apps.Entities
public sealed class AppProvider : IAppProvider
{
private readonly IGrainFactory grainFactory;
private readonly IAppRepository appRepository;
private readonly IRuleRepository ruleRepository;
private readonly IRequestCache requestCache;
private readonly ISchemaRepository schemaRepository;
public AppProvider(
IGrainFactory grainFactory,
IAppRepository appRepository,
ISchemaRepository schemaRepository,
IRuleRepository ruleRepository,
IRequestCache requestCache)
public AppProvider(IGrainFactory grainFactory, IRequestCache requestCache)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
Guard.NotNull(appRepository, nameof(appRepository));
Guard.NotNull(schemaRepository, nameof(schemaRepository));
Guard.NotNull(requestCache, nameof(requestCache));
Guard.NotNull(ruleRepository, nameof(ruleRepository));
this.grainFactory = grainFactory;
this.appRepository = appRepository;
this.schemaRepository = schemaRepository;
this.requestCache = requestCache;
this.ruleRepository = ruleRepository;
}
public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id)
@ -143,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities
{
using (Profile.Method<AppProvider>())
{
var ids = await schemaRepository.QuerySchemaIdsAsync(appId);
var ids = await grainFactory.GetGrain<ISchemasByAppIndex>(appId).GetSchemaIdsAsync();
var schemas =
await Task.WhenAll(
@ -160,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities
{
using (Profile.Method<AppProvider>())
{
var ids = await ruleRepository.QueryRuleIdsAsync(appId);
var ids = await grainFactory.GetGrain<IRulesByAppIndex>(appId).GetRuleIdsAsync();
var rules =
await Task.WhenAll(
@ -177,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities
{
using (Profile.Method<AppProvider>())
{
var ids = await appRepository.QueryUserAppIdsAsync(userId);
var ids = await grainFactory.GetGrain<IAppsByUserIndex>(userId).GetAppIdsAsync();
var apps =
await Task.WhenAll(
@ -192,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities
{
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>())
{
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()
{
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)
{
AddEventMessage<AppContributorAssigned>(
"assigned {user:[Contributor]} as [Permission].");
"assigned {user:[Contributor]} as {[Permission]}");
AddEventMessage<AppContributorRemoved>(
"removed {user:[Contributor]}.");
"removed {user:[Contributor]} from app");
AddEventMessage<AppClientAttached>(
"added client {[Id]}.");
"added client {[Id]} to app");
AddEventMessage<AppClientRevoked>(
"revoked client {[Id]}.");
"revoked client {[Id]}");
AddEventMessage<AppClientUpdated>(
"updated client {[Id]}.");
"updated client {[Id]}");
AddEventMessage<AppClientRenamed>(
"renamed client {[Id]} to {[Name]}.");
"renamed client {[Id]} to {[Name]}");
AddEventMessage<AppLanguageAdded>(
"added language {[Language]}.");
"added language {[Language]}");
AddEventMessage<AppLanguageRemoved>(
"removed language {[Language]}.");
"removed language {[Language]}");
AddEventMessage<AppLanguageUpdated>(
"updated language {[Language]}.");
"updated language {[Language]}");
AddEventMessage<AppMasterLanguageSet>(
"changed master language to {[Language]}.");
"changed master language to {[Language]}");
AddEventMessage<AppPatternAdded>(
"added pattern {[Pattern]}.");
"added pattern {[Name]}");
AddEventMessage<AppPatternDeleted>(
"deleted pattern {[Pattern]}.");
"deleted pattern {[Name]}");
AddEventMessage<AppPatternUpdated>(
"updated pattern {[Pattern]}.");
"updated pattern {[Name]}");
}
protected Task<HistoryEventToStore> On(AppContributorRemoved @event)
@ -146,25 +146,25 @@ namespace Squidex.Domain.Apps.Entities.Apps
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Pattern", @event.Name));
.AddParameter("Name", @event.Name));
}
protected Task<HistoryEventToStore> On(AppPatternDeleted @event)
protected Task<HistoryEventToStore> On(AppPatternUpdated @event)
{
const string channel = "settings.patterns";
return Task.FromResult(
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";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Pattern", @event.Name));
.AddParameter("Name", @event.Name));
}
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
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Domain.Apps.Entities.Rules.Repositories
namespace Squidex.Domain.Apps.Entities.Apps
{
public interface IRuleRepository
public interface IAppsByNameIndex : IGrainWithStringKey
{
Task<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
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Domain.Apps.Entities.Apps.Repositories
namespace Squidex.Domain.Apps.Entities.Apps
{
public interface IAppRepository
public interface IAppsByUserIndex : IGrainWithStringKey
{
Task<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.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Apps.State
{
public class AppState : DomainObjectState<AppState>,
IAppEntity
[CollectionName("Apps")]
public class AppState : DomainObjectState<AppState>, IAppEntity
{
[JsonProperty]
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()
{
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
{
public class AssetState : DomainObjectState<AssetState>,
IAssetEntity,
IAssetInfo
public class AssetState : DomainObjectState<AssetState>, IAssetEntity, IAssetInfo
{
[JsonProperty]
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()
{
return Task.FromResult(new J<List<IBackupJob>>(state.Jobs.OfType<IBackupJob>().ToList()));
return J.AsTask(state.Jobs.OfType<IBackupJob>().ToList());
}
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
{
public class ContentState : DomainObjectState<ContentState>,
IContentEntity
public class ContentState : DomainObjectState<ContentState>, IContentEntity
{
[JsonProperty]
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
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Domain.Apps.Entities.Schemas.Repositories
namespace Squidex.Domain.Apps.Entities.Rules
{
public interface ISchemaRepository
public interface IRulesByAppIndex : IGrainWithGuidKey
{
Task<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)
{
var job = ruleService.CreateJob(ruleEntity.RuleDef, @event);
var job = await ruleService.CreateJobAsync(ruleEntity.RuleDef, @event);
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()
{
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.Dispatching;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules.State
{
public class RuleState : DomainObjectState<RuleState>,
IRuleEntity
[CollectionName("Rules")]
public class RuleState : DomainObjectState<RuleState>, IRuleEntity
{
[JsonProperty]
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()
{
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.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas.State
{
public class SchemaState : DomainObjectState<SchemaState>,
ISchemaEntity
[CollectionName("Schemas")]
public class SchemaState : DomainObjectState<SchemaState>, ISchemaEntity
{
[JsonProperty]
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.
// ==========================================================================
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Infrastructure.MongoDb;
@ -26,7 +28,11 @@ namespace Squidex.Infrastructure.States
protected override string CollectionName()
{
return $"States_{typeof(T).Name}";
var attribute = typeof(T).GetCustomAttributes(true).OfType<CollectionNameAttribute>().FirstOrDefault();
var name = attribute?.Name ?? typeof(T).Name;
return $"States_{name}";
}
public async Task<(T Value, long Version)> ReadAsync(TKey key)
@ -47,5 +53,10 @@ namespace Squidex.Infrastructure.States
{
return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.Doc, value));
}
public Task ReadAllAsync(System.Func<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
{
[BsonIgnoreExtraElements]
public sealed class MongoState<T, TKey> : IVersionedEntity<TKey>
{
[BsonId]

1
src/Squidex.Infrastructure/CachingProviderBase.cs

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

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

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

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

@ -7,6 +7,7 @@
using System;
using Orleans;
using Squidex.Infrastructure.Orleans;
namespace Squidex.Infrastructure.EventSourcing.Grains
{
@ -20,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
eventConsumerManagerGrain = new Lazy<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
{
var grain = grainFactory.GetGrain<T>("Default");
var grain = grainFactory.GetGrain<T>(SingleGrain.Id);
await grain.ActivateAsync();

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Newtonsoft.Json;
#pragma warning disable SA1401 // Fields must be private
@ -14,5 +15,15 @@ namespace Squidex.Infrastructure.Orleans
public static class J
{
public static JsonSerializer Serializer = new JsonSerializer();
public static J<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
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Core.Rules
namespace Squidex.Infrastructure.Orleans
{
public sealed class RuleJobData : Dictionary<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.
// ==========================================================================
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Entities.Contents
namespace Squidex.Infrastructure.States
{
public interface IContentScheduleItem
[AttributeUsage(AttributeTargets.Class)]
public sealed class CollectionNameAttribute : Attribute
{
Status ScheduledTo { get; }
public string Name { get; }
Instant ScheduledAt { get; }
RefToken ScheduledBy { get; }
public CollectionNameAttribute(string name)
{
Name = name;
}
}
}

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

@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.States
{
public sealed class DefaultStreamNameResolver : IStreamNameResolver
{
private static readonly string[] Suffixes = { "Grain", "DomainObject" };
private static readonly string[] Suffixes = { "Grain", "DomainObject", "State" };
public string GetStreamName(Type aggregateType, string id)
{

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.States
@ -16,5 +17,7 @@ namespace Squidex.Infrastructure.States
Task<(T Value, long Version)> ReadAsync(TKey key);
Task ClearAsync();
Task ReadAllAsync(Func<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.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.EventConsumers
@ -28,7 +29,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
public EventConsumersController(ICommandBus commandBus, IGrainFactory grainFactory)
: base(commandBus)
{
eventConsumerManagerGrain = grainFactory.GetGrain<IEventConsumerManagerGrain>("Default");
eventConsumerManagerGrain = grainFactory.GetGrain<IEventConsumerManagerGrain>(SingleGrain.Id);
}
[HttpGet]

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

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

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

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

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.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Backup;
@ -28,7 +29,9 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Indexes;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Migrations;
@ -116,25 +119,22 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<CreateProfileCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<JintScriptEngine>()
.As<IScriptEngine>();
services.AddSingleton<Func<IGrainCallContext, string>>(DomainObjectGrainFormatter.Format);
services.AddSingletonAs<AppsByNameIndexCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddTransientAs<AppGrain>()
.AsSelf();
services.AddSingletonAs<AppsByUserIndexCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddTransientAs<AssetGrain>()
.AsSelf();
services.AddSingletonAs<RulesByAppIndexCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddTransientAs<ContentGrain>()
.AsSelf();
services.AddSingletonAs<SchemasByAppIndexCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddTransientAs<RuleGrain>()
.AsSelf();
services.AddSingletonAs<JintScriptEngine>()
.As<IScriptEngine>();
services.AddTransientAs<SchemaGrain>()
.AsSelf();
services.AddSingleton<Func<IGrainCallContext, string>>(DomainObjectGrainFormatter.Format);
services.AddSingleton(c =>
{
@ -172,7 +172,10 @@ namespace Squidex.Config.Domain
services.AddTransientAs<ConvertEventStoreAppId>()
.As<IMigration>();
services.AddTransientAs<DeleteArchiveCollection>()
services.AddTransientAs<DeleteArchiveCollectionSetup>()
.As<IMigration>();
services.AddTransientAs<PopulateGrainIndexes>()
.As<IMigration>();
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.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Migrate_01.Migrations;
using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Domain.Apps.Entities.Apps.State;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Backup.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.History.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Apps;
using Squidex.Domain.Apps.Entities.MongoDb.Assets;
using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.MongoDb.History;
using Squidex.Domain.Apps.Entities.MongoDb.Rules;
using Squidex.Domain.Apps.Entities.MongoDb.Schemas;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Entities.Rules.State;
using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Domain.Apps.Entities.Schemas.State;
using Squidex.Domain.Users;
using Squidex.Domain.Users.MongoDb;
using Squidex.Domain.Users.MongoDb.Infrastructure;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.UsageTracking;
@ -62,63 +52,55 @@ namespace Squidex.Config.Domain
var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName);
var mongoContentDatabase = mongoClient.GetDatabase(mongoContentDatabaseName);
services.AddSingletonAs(c => new MongoXmlRepository(mongoDatabase))
.As<IXmlRepository>()
.As<IInitializable>();
services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>));
services.AddSingletonAs(c => new MongoMigrationStatus(mongoDatabase))
.As<IMigrationStatus>()
.As<IInitializable>();
services.AddSingletonAs(mongoDatabase)
.As<IMongoDatabase>();
services.AddSingletonAs(c => new MongoSnapshotStore<BackupState, Guid>(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore<BackupState, Guid>>()
services.AddSingletonAs<MongoXmlRepository>()
.As<IXmlRepository>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoSnapshotStore<EventConsumerState, string>(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore<EventConsumerState, string>>()
services.AddSingletonAs<MongoMigrationStatus>()
.As<IMigrationStatus>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoPersistedGrantStore(mongoDatabase))
services.AddSingletonAs<MongoPersistedGrantStore>()
.As<IPersistedGrantStore>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoUsageStore(mongoDatabase))
services.AddSingletonAs<MongoUsageStore>()
.As<IUsageStore>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase))
services.AddSingletonAs<MongoRuleEventRepository>()
.As<IRuleEventRepository>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoUserStore(mongoDatabase))
services.AddSingletonAs<MongoUserStore>()
.As<IUserStore<IUser>>()
.As<IUserFactory>()
.As<IUserResolver>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoRoleStore(mongoDatabase))
services.AddSingletonAs<MongoRoleStore>()
.As<IRoleStore<IRole>>()
.As<IRoleFactory>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase))
.As<IAppRepository>()
.As<ISnapshotStore<AppState, Guid>>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase))
.As<IAssetRepository>()
.As<ISnapshotStore<AssetState, Guid>>()
services.AddSingletonAs<MongoHistoryEventRepository>()
.As<IHistoryEventRepository>()
.As<IEventConsumer>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoRuleRepository(mongoDatabase))
.As<IRuleRepository>()
.As<ISnapshotStore<RuleState, Guid>>()
services.AddSingletonAs<MongoAssetStatsRepository>()
.As<IAssetStatsRepository>()
.As<IEventConsumer>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase))
.As<ISchemaRepository>()
.As<ISnapshotStore<SchemaState, Guid>>()
services.AddSingletonAs<MongoAssetRepository>()
.As<IAssetRepository>()
.As<ISnapshotStore<AssetState, Guid>>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService<IAppProvider>()))
@ -127,15 +109,11 @@ namespace Squidex.Config.Domain
.As<IEventConsumer>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices<IHistoryEventsCreator>()))
.As<IHistoryEventRepository>()
.As<IEventConsumer>()
.As<IInitializable>();
services.AddTransientAs(c => new DeleteArchiveCollectionSetup(mongoContentDatabase))
.As<IMigration>();
services.AddSingletonAs(c => new MongoAssetStatsRepository(mongoDatabase))
.As<IAssetStatsRepository>()
.As<IEventConsumer>()
.As<IInitializable>();
services.AddTransientAs<ConvertOldSnapshotStores>()
.As<IMigration>();
}
});

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

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

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

@ -79,7 +79,7 @@ namespace Squidex.Pipeline.Swagger
public static SwaggerSecurityScheme CreateOAuthSchema(MyUrlsOptions urlOptions)
{
var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token");
var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false);
var securityDocs = LoadDocs("security");
var securityText = securityDocs.Replace("<TOKEN_URL>", tokenUrl);

1
src/Squidex/Program.cs

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

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>
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="selectedFormControl" [maximumStars]="field.properties['maxValue']"></sqx-stars>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" [value]="value" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" sqxSlugifyInput />
</div>
<div *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</div>
<div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="selectedFormControl"></sqx-rich-editor>
</div>
<div *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="selectedFormControl"></sqx-markdown-editor>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="selectedFormControl"></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControl]="selectedFormControl" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties['editor']" [formControl]="selectedFormControl"></sqx-date-time-editor>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="selectedFormControl"></sqx-geolocation-editor>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-editor [formControl]="selectedFormControl"></sqx-json-editor>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="selectedFormControl"></sqx-assets-editor>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="selectedFormControl"></sqx-tag-editor>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-editor
[formControl]="selectedFormControl"
[language]="language"
[languages]="languages"
[schemaId]="field.properties['schemaId']">
</sqx-references-editor>
</div>
<div>
<ng-container *ngIf="field.properties.editorUrl">
<sqx-iframe-editor [url]="field.properties.editorUrl" [formControl]="selectedFormControl"></sqx-iframe-editor>
</ng-container>
<ng-container *ngIf="!field.properties.editorUrl">
<ng-container [ngSwitch]="field.properties.fieldType">
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="selectedFormControl" [maximumStars]="field.properties['maxValue']"></sqx-stars>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<ng-container class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" [value]="value" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify" />
</ng-container>
<ng-container *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="selectedFormControl"></sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="selectedFormControl"></sqx-markdown-editor>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<ng-container class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="selectedFormControl"></sqx-toggle>
</ng-container>
<ng-container *ngSwitchCase="'Checkbox'">
<ng-container class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControl]="selectedFormControl" sqxIndeterminateValue />
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties['editor']" [formControl]="selectedFormControl"></sqx-date-time-editor>
</ng-container>
<ng-container *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="selectedFormControl"></sqx-geolocation-editor>
</ng-container>
<ng-container *ngSwitchCase="'Json'">
<sqx-json-editor [formControl]="selectedFormControl"></sqx-json-editor>
</ng-container>
<ng-container *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="selectedFormControl"></sqx-assets-editor>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="selectedFormControl"></sqx-tag-editor>
</ng-container>
<ng-container *ngSwitchCase="'References'">
<sqx-references-editor
[formControl]="selectedFormControl"
[language]="language"
[languages]="languages"
[schemaId]="field.properties['schemaId']">
</sqx-references-editor>
</ng-container>
</ng-container>
</ng-container>
</div>
<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[]) {
if (Types.isArrayOfString(value) && !Types.isEquals(value, this.oldAssets.map(x => x.id).values)) {
const assetIds: string[] = value;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, value)
.subscribe(dtos => {
this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!));
if (this.oldAssets.length !== assetIds.length) {
this.updateValue();
}
}, () => {
this.oldAssets = ImmutableArray.empty<AssetDto>();
});
public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) {
if (!Types.isEquals(obj, this.oldAssets.map(x => x.id).values)) {
const assetIds: string[] = obj;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, obj)
.subscribe(dtos => {
this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!));
if (this.oldAssets.length !== assetIds.length) {
this.updateValue();
}
}, () => {
this.oldAssets = ImmutableArray.empty();
});
}
} else {
this.oldAssets = ImmutableArray.empty();
}
}

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" />
</div>
<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 *ngSwitchCase="'Dropdown'">
<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[]) {
if (Types.isArrayOfString(value) && !Types.isEquals(value, this.contentItems.map(x => x.id).values)) {
const contentIds: string[] = value;
this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds)
.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();
}
}, () => {
this.contentItems = ImmutableArray.empty<ContentDto>();
});
public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) {
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.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();
}
}, () => {
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>
</div>
</div>
<div class="col">
<div class="col-lg">
<div class="type-title">{{fieldType.type}}</div>
<div class="type-text text-muted">{{fieldType.description}}</div>
</div>

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

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

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

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

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

@ -1,4 +1,15 @@
<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">
<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
]));
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.setControl('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 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">
<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, [
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 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">
<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">
<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>

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

@ -1,2 +1,6 @@
@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 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">
<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
]));
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.setControl('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 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">
<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
]));
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.setControl('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>
<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">
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">
<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 class="col col-auto pl-1">
<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,
AutocompleteSource,
ContributorsState,
Types,
UserDto,
UsersService
} from '@app/shared';
@ -85,7 +86,7 @@ export class ContributorsPageComponent implements OnInit {
if (value) {
let user = value.user;
if (user instanceof UserDto) {
if (Types.is(user, UserDto)) {
user = user.id;
}

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

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

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

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

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

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

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

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

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Directive, ElementRef, EventEmitter, HostListener, Output, Renderer } from '@angular/core';
import { Directive, ElementRef, EventEmitter, HostListener, Output, Renderer2 } from '@angular/core';
@Directive({
selector: '[sqxFileDrop]'
@ -18,7 +18,7 @@ export class FileDropDirective {
constructor(
private readonly element: ElementRef,
private readonly renderer: Renderer
private readonly renderer: Renderer2
) {
}
@ -71,7 +71,7 @@ export class FileDropDirective {
this.dragCounter++;
if (this.dragCounter === 1) {
this.renderer.setElementClass(this.element.nativeElement, 'drag', true);
this.renderer.addClass(this.element.nativeElement, 'drag');
}
}
@ -79,7 +79,7 @@ export class FileDropDirective {
this.dragCounter = number || this.dragCounter - 1;
if (this.dragCounter === 0) {
this.renderer.setElementClass(this.element.nativeElement, 'drag', false);
this.renderer.removeClass(this.element.nativeElement, 'drag');
}
}

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

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.
*/
import { AfterViewInit, Directive, ElementRef, Input, Renderer } from '@angular/core';
import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';
import { Types } from '@app/framework/internal';
@Directive({
selector: '[sqxFocusOnInit]'
@ -15,17 +17,20 @@ export class FocusOnInitDirective implements AfterViewInit {
public select: boolean;
constructor(
private readonly element: ElementRef,
private readonly renderer: Renderer
private readonly element: ElementRef
) {
}
public ngAfterViewInit() {
setTimeout(() => {
this.renderer.invokeElementMethod(this.element.nativeElement, 'focus', []);
if (Types.isFunction(this.element.nativeElement.focus)) {
this.element.nativeElement.focus();
}
if (this.select) {
this.renderer.invokeElementMethod(this.element.nativeElement, 'select', []);
if (Types.isFunction(this.element.nativeElement.select)) {
this.element.nativeElement.select();
}
}
}, 100);
}

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

Loading…
Cancel
Save