// ==========================================================================
// 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;
}
}
}