// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschränkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Contents { public sealed class ContentsController : ApiController { private readonly IOptions controllerOptions; private readonly IContentQueryService contentQuery; private readonly IContentWorkflow contentWorkflow; private readonly IGraphQLService graphQl; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IContentWorkflow contentWorkflow, IGraphQLService graphQl, IOptions controllerOptions) : base(commandBus) { this.contentQuery = contentQuery; this.contentWorkflow = contentWorkflow; this.controllerOptions = controllerOptions; this.graphQl = graphQl; } /// /// GraphQL endpoint. /// /// The name of the app. /// The graphql query. /// /// 200 => Contents retrieved or mutated. /// 404 => Schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpGet] [HttpPost] [Route("content/{app}/graphql/")] [ApiPermission] [ApiCosts(2)] public async Task PostGraphQL(string app, [FromBody] GraphQLQuery query) { var result = await graphQl.QueryAsync(Context, query); if (result.HasError) { return BadRequest(result.Response); } else { return Ok(result.Response); } } /// /// GraphQL endpoint (Batch). /// /// The name of the app. /// The graphql queries. /// /// 200 => Contents retrieved or mutated. /// 404 => Schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpGet] [HttpPost] [Route("content/{app}/graphql/batch")] [ApiPermission] [ApiCosts(2)] public async Task PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch) { var result = await graphQl.QueryAsync(Context, batch); if (result.HasError) { return BadRequest(result.Response); } else { return Ok(result.Response); } } /// /// Queries contents. /// /// The name of the app. /// The optional ids of the content to fetch. /// /// 200 => Contents retrieved. /// 404 => App not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpGet] [Route("content/{app}/")] [ProducesResponseType(typeof(ContentsDto), 200)] [ApiPermission] [ApiCosts(1)] public async Task GetAllContents(string app, [FromQuery] string ids) { var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids).Ids); var response = await ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) { Response.Headers["Surrogate-Key"] = response.ToSurrogateKeys(); } Response.Headers[HeaderNames.ETag] = $"{response.ToEtag()}_{App.Version}"; return Ok(response); } /// /// Queries contents. /// /// The name of the app. /// The name of the schema. /// The optional ids of the content to fetch. /// /// 200 => Contents retrieved. /// 404 => Schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpGet] [Route("content/{app}/{name}/")] [ProducesResponseType(typeof(ContentsDto), 200)] [ApiPermission] [ApiCosts(1)] public async Task GetContents(string app, string name, [FromQuery] string ids = null) { var contents = await contentQuery.QueryAsync(Context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString())); var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); var response = await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); if (ShouldProvideSurrogateKeys(response)) { Response.Headers["Surrogate-Key"] = response.ToSurrogateKeys(); } Response.Headers[HeaderNames.ETag] = $"{response.ToEtag()}_{App.Version}"; return Ok(response); } /// /// Get a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content to fetch. /// /// 200 => Content found. /// 404 => Content, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpGet] [Route("content/{app}/{name}/{id}/")] [ProducesResponseType(typeof(ContentsDto), 200)] [ApiPermission] [ApiCosts(1)] public async Task GetContent(string app, string name, Guid id) { var content = await contentQuery.FindContentAsync(Context, name, id); var response = ContentDto.FromContent(Context, content, this); if (controllerOptions.Value.EnableSurrogateKeys) { Response.Headers["Surrogate-Key"] = content.Id.ToString(); } Response.Headers[HeaderNames.ETag] = $"{response.ToEtag()}_{App.Version}"; return Ok(response); } /// /// Get a content by version. /// /// The name of the app. /// The name of the schema. /// The id of the content to fetch. /// The version fo the content to fetch. /// /// 200 => Content found. /// 404 => Content, schema or app not found. /// 400 => Content data is not valid. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpGet] [Route("content/{app}/{name}/{id}/{version}/")] [ApiPermission(Permissions.AppContentsRead)] [ApiCosts(1)] public async Task GetContentVersion(string app, string name, Guid id, int version) { var content = await contentQuery.FindContentAsync(Context, name, id, version); var response = ContentDto.FromContent(Context, content, this); if (controllerOptions.Value.EnableSurrogateKeys) { Response.Headers["Surrogate-Key"] = content.Id.ToString(); } Response.Headers[HeaderNames.ETag] = $"{response.ToEtag()}_{App.Version}"; return Ok(response.Data); } /// /// Create a content item. /// /// The name of the app. /// The name of the schema. /// The full data for the content item. /// Indicates whether the content should be published immediately. /// /// 201 => Content created. /// 404 => Content, schema or app not found. /// 400 => Content data is not valid. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/{name}/")] [ProducesResponseType(typeof(ContentsDto), 200)] [ApiPermission(Permissions.AppContentsCreate)] [ApiCosts(1)] public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) { await contentQuery.GetSchemaOrThrowAsync(Context, name); if (publish && !this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) { return new ForbidResult(); } var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; var response = await InvokeCommandAsync(app, name, command); return CreatedAtAction(nameof(GetContent), new { id = command.ContentId }, response); } /// /// Update a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to update. /// The full data for the content item. /// Indicates whether the update is a proposal. /// /// 200 => Content updated. /// 404 => Content, schema or app not found. /// 400 => Content data is not valid. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPut] [Route("content/{app}/{name}/{id}/")] [ProducesResponseType(typeof(ContentsDto), 200)] [ApiPermission(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { await contentQuery.GetSchemaOrThrowAsync(Context, name); var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; var response = await InvokeCommandAsync(app, name, command); return Ok(response); } /// /// Patchs a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to patch. /// The patch for the content item. /// Indicates whether the patch is a proposal. /// /// 200 => Content patched. /// 404 => Content, schema or app not found. /// 400 => Content patch is not valid. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPatch] [Route("content/{app}/{name}/{id}/")] [ProducesResponseType(typeof(ContentsDto), 200)] [ApiPermission(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { await contentQuery.GetSchemaOrThrowAsync(Context, name); var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; var response = await InvokeCommandAsync(app, name, command); return Ok(response); } /// /// Publish a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to publish. /// The status request. /// /// 200 => Content published. /// 404 => Content, schema or app not found. /// 400 => Request is not valid. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPut] [Route("content/{app}/{name}/{id}/status/")] [ProducesResponseType(typeof(ContentsDto), 200)] [ApiPermission] [ApiCosts(1)] public async Task PutContentStatus(string app, string name, Guid id, ChangeStatusDto request) { await contentQuery.GetSchemaOrThrowAsync(Context, name); if (!this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) { return new ForbidResult(); } var command = request.ToCommand(id); var response = await InvokeCommandAsync(app, name, command); return Ok(response); } /// /// Discard changes. /// /// The name of the app. /// The name of the schema. /// The id of the content item to discard changes. /// /// 200 => Content restored. /// 404 => Content, schema or app not found. /// 400 => Content was not archived. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPut] [Route("content/{app}/{name}/{id}/discard/")] [ProducesResponseType(typeof(ContentsDto), 200)] [ApiPermission(Permissions.AppContentsDiscard)] [ApiCosts(1)] public async Task DiscardDraft(string app, string name, Guid id) { await contentQuery.GetSchemaOrThrowAsync(Context, name); var command = new DiscardChanges { ContentId = id }; var response = await InvokeCommandAsync(app, name, command); return Ok(response); } /// /// Delete a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to delete. /// /// 204 => Content deleted. /// 404 => Content, schema or app not found. /// /// /// You can create an generated documentation for your app at /api/content/{appName}/docs. /// [HttpDelete] [Route("content/{app}/{name}/{id}/")] [ApiPermission(Permissions.AppContentsDelete)] [ApiCosts(1)] public async Task DeleteContent(string app, string name, Guid id) { await contentQuery.GetSchemaOrThrowAsync(Context, name); var command = new DeleteContent { ContentId = id }; await CommandBus.PublishAsync(command); return NoContent(); } private async Task InvokeCommandAsync(string app, string schema, ICommand command) { var context = await CommandBus.PublishAsync(command); var result = context.Result(); var response = ContentDto.FromContent(Context, result, this); return response; } private bool ShouldProvideSurrogateKeys(ContentsDto response) { return controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys; } } }