// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using NodaTime; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Rules.Runner; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules { /// /// Manages and retrieves information about schemas. /// [ApiExplorerSettings(GroupName = nameof(Rules))] public sealed class RulesController : ApiController { private readonly EventJsonSchemaGenerator eventJsonSchemaGenerator; private readonly IAppProvider appProvider; private readonly IRuleEventRepository ruleEventsRepository; private readonly IRuleQueryService ruleQuery; private readonly IRuleRunnerService ruleRunnerService; private readonly RuleTypeProvider ruleRegistry; public RulesController(ICommandBus commandBus, IAppProvider appProvider, IRuleEventRepository ruleEventsRepository, IRuleQueryService ruleQuery, IRuleRunnerService ruleRunnerService, RuleTypeProvider ruleRegistry, EventJsonSchemaGenerator eventJsonSchemaGenerator) : base(commandBus) { this.appProvider = appProvider; this.ruleEventsRepository = ruleEventsRepository; this.ruleQuery = ruleQuery; this.ruleRunnerService = ruleRunnerService; this.ruleRegistry = ruleRegistry; this.eventJsonSchemaGenerator = eventJsonSchemaGenerator; } /// /// Get supported rule actions. /// /// /// 200 => Rule actions returned. /// [HttpGet] [Route("rules/actions/")] [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] [ApiPermission] [ApiCosts(0)] public IActionResult GetActions() { var etag = string.Concat(ruleRegistry.Actions.Select(x => x.Key)).ToSha256Base64(); var response = Deferred.Response(() => { return ruleRegistry.Actions.ToDictionary(x => x.Key, x => RuleElementDto.FromDomain(x.Value)); }); Response.Headers[HeaderNames.ETag] = etag; return Ok(response); } /// /// Get rules. /// /// The name of the app. /// /// 200 => Rules returned. /// 404 => App not found. /// [HttpGet] [Route("apps/{app}/rules/")] [ProducesResponseType(typeof(RulesDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesRead)] [ApiCosts(1)] public async Task GetRules(string app) { var rules = await ruleQuery.QueryAsync(Context, HttpContext.RequestAborted); var response = Deferred.AsyncResponse(() => { return RulesDto.FromRulesAsync(rules, ruleRunnerService, Resources); }); return Ok(response); } /// /// Create a new rule. /// /// The name of the app. /// The rule object that needs to be added to the app. /// /// 201 => Rule created. /// 400 => Rule request not valid. /// 404 => App not found. /// [HttpPost] [Route("apps/{app}/rules/")] [ProducesResponseType(typeof(RuleDto), 201)] [ApiPermissionOrAnonymous(Permissions.AppRulesCreate)] [ApiCosts(1)] public async Task PostRule(string app, [FromBody] CreateRuleDto request) { var command = request.ToCommand(); var response = await InvokeCommandAsync(command); return CreatedAtAction(nameof(GetRules), new { app }, response); } /// /// Cancel the current run. /// /// The name of the app. /// /// 204 => Rule run cancelled. /// [HttpDelete] [Route("apps/{app}/rules/run")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsUpdate)] [ApiCosts(1)] public async Task DeleteRuleRun(string app) { await ruleRunnerService.CancelAsync(App.Id, HttpContext.RequestAborted); return NoContent(); } /// /// Update a rule. /// /// The name of the app. /// The id of the rule to update. /// The rule object that needs to be added to the app. /// /// 200 => Rule updated. /// 400 => Rule request not valid. /// 404 => Rule or app not found. /// [HttpPut] [Route("apps/{app}/rules/{id}/")] [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesUpdate)] [ApiCosts(1)] public async Task PutRule(string app, DomainId id, [FromBody] UpdateRuleDto request) { var command = request.ToCommand(id); var response = await InvokeCommandAsync(command); return Ok(response); } /// /// Enable a rule. /// /// The name of the app. /// The id of the rule to enable. /// /// 200 => Rule enabled. /// 404 => Rule or app not found. /// [HttpPut] [Route("apps/{app}/rules/{id}/enable/")] [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesDisable)] [ApiCosts(1)] public async Task EnableRule(string app, DomainId id) { var command = new EnableRule { RuleId = id }; var response = await InvokeCommandAsync(command); return Ok(response); } /// /// Disable a rule. /// /// The name of the app. /// The id of the rule to disable. /// /// 200 => Rule disabled. /// 404 => Rule or app not found. /// [HttpPut] [Route("apps/{app}/rules/{id}/disable/")] [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesDisable)] [ApiCosts(1)] public async Task DisableRule(string app, DomainId id) { var command = new DisableRule { RuleId = id }; var response = await InvokeCommandAsync(command); return Ok(response); } /// /// Trigger a rule. /// /// The name of the app. /// The id of the rule to disable. /// /// 204 => Rule triggered. /// 404 => Rule or app not found. /// [HttpPut] [Route("apps/{app}/rules/{id}/trigger/")] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsRun)] [ApiCosts(1)] public async Task TriggerRule(string app, DomainId id) { var command = new TriggerRule { RuleId = id }; await CommandBus.PublishAsync(command); return NoContent(); } /// /// Run a rule. /// /// The name of the app. /// The id of the rule to run. /// Runs the rule from snapeshots if possible. /// /// 204 => Rule started. /// [HttpPut] [Route("apps/{app}/rules/{id}/run")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsRun)] [ApiCosts(1)] public async Task PutRuleRun(string app, DomainId id, [FromQuery] bool fromSnapshots = false) { await ruleRunnerService.RunAsync(App.Id, id, fromSnapshots, HttpContext.RequestAborted); return NoContent(); } /// /// Cancels all rule events. /// /// The name of the app. /// The id of the rule to cancel. /// /// 204 => Rule events cancelled. /// [HttpDelete] [Route("apps/{app}/rules/{id}/events/")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsDelete)] [ApiCosts(1)] public async Task DeleteRuleEvents(string app, DomainId id) { await ruleEventsRepository.CancelByRuleAsync(id, HttpContext.RequestAborted); return NoContent(); } /// /// Simulate a rule. /// /// The name of the app. /// The rule to simulate. /// /// 200 => Rule simulated. /// 404 => Rule or app not found. /// [HttpPost] [Route("apps/{app}/rules/simulate/")] [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsRead)] [ApiCosts(5)] public async Task Simulate(string app, [FromBody] CreateRuleDto request) { var rule = request.ToRule(); var simulation = await ruleRunnerService.SimulateAsync(App.NamedId(), DomainId.Empty, rule, HttpContext.RequestAborted); var response = SimulatedRuleEventsDto.FromDomain(simulation); return Ok(response); } /// /// Simulate a rule. /// /// The name of the app. /// The id of the rule to simulate. /// /// 200 => Rule simulated. /// 404 => Rule or app not found. /// [HttpGet] [Route("apps/{app}/rules/{id}/simulate/")] [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsRead)] [ApiCosts(5)] public async Task Simulate(string app, DomainId id) { var rule = await appProvider.GetRuleAsync(AppId, id, HttpContext.RequestAborted); if (rule == null) { return NotFound(); } var simulation = await ruleRunnerService.SimulateAsync(rule, HttpContext.RequestAborted); var response = SimulatedRuleEventsDto.FromDomain(simulation); return Ok(response); } /// /// Delete a rule. /// /// The name of the app. /// The id of the rule to delete. /// /// 204 => Rule deleted. /// 404 => Rule or app not found. /// [HttpDelete] [Route("apps/{app}/rules/{id}/")] [ApiPermissionOrAnonymous(Permissions.AppRulesDelete)] [ApiCosts(1)] public async Task DeleteRule(string app, DomainId id) { await CommandBus.PublishAsync(new DeleteRule { RuleId = id }); return NoContent(); } /// /// Get rule events. /// /// The name of the app. /// The optional rule id to filter to events. /// The number of events to skip. /// The number of events to take. /// /// 200 => Rule events returned. /// 404 => App not found. /// [HttpGet] [Route("apps/{app}/rules/events/")] [ProducesResponseType(typeof(RuleEventsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsRead)] [ApiCosts(0)] public async Task GetEvents(string app, [FromQuery] DomainId? ruleId = null, [FromQuery] int skip = 0, [FromQuery] int take = 20) { var ruleEvents = await ruleEventsRepository.QueryByAppAsync(AppId, ruleId, skip, take, HttpContext.RequestAborted); var response = RuleEventsDto.FromDomain(ruleEvents, Resources, ruleId); return Ok(response); } /// /// Retry the event immediately. /// /// The name of the app. /// The event to enqueue. /// /// 204 => Rule enqueued. /// 404 => App or rule event not found. /// [HttpPut] [Route("apps/{app}/rules/events/{id}/")] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsUpdate)] [ApiCosts(0)] public async Task PutEvent(string app, DomainId id) { var ruleEvent = await ruleEventsRepository.FindAsync(id, HttpContext.RequestAborted); if (ruleEvent == null) { return NotFound(); } await ruleEventsRepository.EnqueueAsync(id, SystemClock.Instance.GetCurrentInstant(), HttpContext.RequestAborted); return NoContent(); } /// /// Cancels all events. /// /// The name of the app. /// /// 204 => Events cancelled. /// [HttpDelete] [Route("apps/{app}/rules/events/")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsDelete)] [ApiCosts(1)] public async Task DeleteEvents(string app) { await ruleEventsRepository.CancelByAppAsync(App.Id, HttpContext.RequestAborted); return NoContent(); } /// /// Cancels an event. /// /// The name of the app. /// The event to enqueue. /// /// 204 => Rule deqeued. /// 404 => App or rule event not found. /// [HttpDelete] [Route("apps/{app}/rules/events/{id}/")] [ApiPermissionOrAnonymous(Permissions.AppRulesEventsDelete)] [ApiCosts(0)] public async Task DeleteEvent(string app, DomainId id) { var ruleEvent = await ruleEventsRepository.FindAsync(id, HttpContext.RequestAborted); if (ruleEvent == null) { return NotFound(); } await ruleEventsRepository.CancelByRuleAsync(id, HttpContext.RequestAborted); return NoContent(); } /// /// Provide a list of all event types that are used in rules. /// /// /// 200 => Rule events returned. /// [HttpGet] [Route("rules/eventtypes")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [AllowAnonymous] public IActionResult GetEventTypes() { var types = eventJsonSchemaGenerator.AllTypes; return Ok(types); } /// /// Provide the json schema for the event with the specified name. /// /// The type name of the event. /// /// 200 => Rule event type found. /// 404 => Rule event not found. /// [HttpGet] [Route("rules/eventtypes/{type}")] [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] [AllowAnonymous] public IActionResult GetEventSchema(string type) { var schema = eventJsonSchemaGenerator.GetSchema(type); if (schema == null) { return NotFound(); } return Content(schema.ToJson(), "application/json"); } [HttpGet] [Route("apps/{app}/rules/completion/{triggerType}")] [ApiPermissionOrAnonymous] [ApiCosts(1)] [OpenApiIgnore] public IActionResult GetScriptCompletion(string app, string triggerType, [FromServices] ScriptingCompleter completer) { var completion = completer.Trigger(triggerType); return Ok(completion); } private async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command); var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(Context.App.Id, HttpContext.RequestAborted); var result = context.Result(); var response = RuleDto.FromDomain(result, runningRuleId == null, ruleRunnerService, Resources); return response; } } }