// ========================================================================== // 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 Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using NodaTime; using NodaTime.Text; 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.Shared.Identity; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Contents { public sealed class ContentsController : ApiController { private readonly IOptions controllerOptions; private readonly IContentQueryService contentQuery; private readonly IGraphQLService graphQl; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IGraphQLService graphQl, IOptions controllerOptions) : base(commandBus) { this.contentQuery = contentQuery; 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. /// The requested status, only for frontend client. /// /// 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}/")] [ApiPermission] [ApiCosts(1)] public async Task GetAllContents(string app, [FromQuery] string ids, [FromQuery] string status = null) { var context = Context().WithFrontendStatus(status); var result = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids); var response = new ContentsDto { Total = result.Count, Items = result.Take(200).Select(x => ContentDto.FromContent(x, context)).ToArray() }; if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) { Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys(); } Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag(); return Ok(response); } /// /// Queries contents. /// /// The name of the app. /// The name of the schema. /// The optional ids of the content to fetch. /// The requested status, only for frontend client. /// /// 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}/")] [ApiPermission] [ApiCosts(1)] public async Task GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] string status = null) { var context = Context().WithFrontendStatus(status); var result = await contentQuery.QueryAsync(context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString())); var response = new ContentsDto { Total = result.Total, Items = result.Take(200).Select(x => ContentDto.FromContent(x, context)).ToArray() }; if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) { Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys(); } Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag(response.Total); 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}/")] [ApiPermission] [ApiCosts(1)] public async Task GetContent(string app, string name, Guid id) { var context = Context(); var content = await contentQuery.FindContentAsync(context, name, id); var response = ContentDto.FromContent(content, context); if (controllerOptions.Value.EnableSurrogateKeys) { Response.Headers["Surrogate-Key"] = content.Id.ToString(); } Response.Headers[HeaderNames.ETag] = content.Version.ToString(); 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 context = Context(); var content = await contentQuery.FindContentAsync(context, name, id, version); var response = ContentDto.FromContent(content, context); if (controllerOptions.Value.EnableSurrogateKeys) { Response.Headers["Surrogate-Key"] = content.Id.ToString(); } Response.Headers[HeaderNames.ETag] = content.Version.ToString(); 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}/")] [ApiPermission(Permissions.AppContentsCreate)] [ApiCosts(1)] public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) { await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); if (publish && !this.HasPermission(Permissions.AppContentsPublish, app, name)) { return new StatusCodeResult(123); } var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; var context = await CommandBus.PublishAsync(command); var result = context.Result>(); var response = ContentDto.FromCommand(command, result); 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}/")] [ApiPermission(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; var context = await CommandBus.PublishAsync(command); var result = context.Result(); var response = result.Data; 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}/")] [ApiPermission(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; var context = await CommandBus.PublishAsync(command); var result = context.Result(); var response = result.Data; 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 date and time when the content should be published. /// /// 204 => Content published. /// 404 => Content, schema or app not found. /// 400 => Content was already published. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPut] [Route("content/{app}/{name}/{id}/publish/")] [ApiPermission(Permissions.AppContentsPublish)] [ApiCosts(1)] public async Task PublishContent(string app, string name, Guid id, string dueTime = null) { await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = CreateCommand(id, Status.Published, dueTime); await CommandBus.PublishAsync(command); return NoContent(); } /// /// Unpublish a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to unpublish. /// The date and time when the content should be unpublished. /// /// 204 => Content unpublished. /// 404 => Content, schema or app not found. /// 400 => Content was not published. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPut] [Route("content/{app}/{name}/{id}/unpublish/")] [ApiPermission(Permissions.AppContentsUnpublish)] [ApiCosts(1)] public async Task UnpublishContent(string app, string name, Guid id, string dueTime = null) { await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = CreateCommand(id, Status.Draft, dueTime); await CommandBus.PublishAsync(command); return NoContent(); } /// /// Archive a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to archive. /// The date and time when the content should be archived. /// /// 204 => Content archived. /// 404 => Content, schema or app not found. /// 400 => Content was already archived. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPut] [Route("content/{app}/{name}/{id}/archive/")] [ApiPermission(Permissions.AppContentsArchive)] [ApiCosts(1)] public async Task ArchiveContent(string app, string name, Guid id, string dueTime = null) { await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = CreateCommand(id, Status.Archived, dueTime); await CommandBus.PublishAsync(command); return NoContent(); } /// /// Restore a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to restore. /// The date and time when the content should be restored. /// /// 204 => 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}/restore/")] [ApiPermission(Permissions.AppContentsRestore)] [ApiCosts(1)] public async Task RestoreContent(string app, string name, Guid id, string dueTime = null) { await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = CreateCommand(id, Status.Draft, dueTime); await CommandBus.PublishAsync(command); return NoContent(); } /// /// Discard changes. /// /// The name of the app. /// The name of the schema. /// The id of the content item to discard changes. /// /// 204 => 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/")] [ApiPermission(Permissions.AppContentsDiscard)] [ApiCosts(1)] public async Task DiscardChanges(string app, string name, Guid id) { await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = new DiscardChanges { ContentId = id }; await CommandBus.PublishAsync(command); return NoContent(); } /// /// Delete a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to delete. /// /// 204 => Content has been 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.ThrowIfSchemaNotExistsAsync(Context(), name); var command = new DeleteContent { ContentId = id }; await CommandBus.PublishAsync(command); return NoContent(); } private static ChangeContentStatus CreateCommand(Guid id, Status status, string dueTime) { Instant? dt = null; if (!string.IsNullOrWhiteSpace(dueTime)) { var parseResult = InstantPattern.General.Parse(dueTime); if (parseResult.Success) { dt = parseResult.Value; } } return new ChangeContentStatus { Status = status, ContentId = id, DueTime = dt }; } private QueryContext Context() { return QueryContext.Create(App, User) .WithAssetUrlsToResolve(Request.Headers["X-Resolve-Urls"]) .WithFlatten(Request.Headers.ContainsKey("X-Flatten")) .WithLanguages(Request.Headers["X-Languages"]) .WithUnpublished(Request.Headers.ContainsKey("X-Unpublished")); } } }