// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschränkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; 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; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Contents { public sealed class ContentsController : ApiController { private readonly IContentQueryService contentQuery; private readonly IContentWorkflow contentWorkflow; private readonly IGraphQLService graphQl; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IContentWorkflow contentWorkflow, IGraphQLService graphQl) : base(commandBus) { this.contentQuery = contentQuery; this.contentWorkflow = contentWorkflow; this.graphQl = graphQl; } /// /// GraphQL endpoint. /// /// The name of the app. /// The graphql query. /// /// 200 => Contents returned or mutated. /// 404 => App not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpGet] [Route("content/{app}/graphql/")] [ApiPermissionOrAnonymous] [ApiCosts(2)] public async Task GetGraphQL(string app, [FromQuery] GraphQLGetDto? queries = null) { var request = queries?.ToQuery() ?? new GraphQLQuery(); var (hasError, response) = await graphQl.QueryAsync(Context, request); if (hasError) { return BadRequest(response); } else { return Ok(response); } } /// /// GraphQL endpoint. /// /// The name of the app. /// The graphql query. /// /// 200 => Contents returned or mutated. /// 404 => App not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/graphql/")] [ApiPermissionOrAnonymous] [ApiCosts(2)] public async Task PostGraphQL(string app, [FromBody] GraphQLPostDto query) { var request = query?.ToQuery() ?? new GraphQLQuery(); var (hasError, response) = await graphQl.QueryAsync(Context, request); if (hasError) { return BadRequest(response); } else { return Ok(response); } } /// /// GraphQL endpoint (Batch). /// /// The name of the app. /// The graphql queries. /// /// 200 => Contents returned or mutated. /// 404 => App not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/graphql/batch")] [ApiPermissionOrAnonymous] [ApiCosts(2)] public async Task PostGraphQLBatch(string app, [FromBody] GraphQLPostDto[] batch) { var request = batch.Select(x => x.ToQuery()).ToArray(); var (hasError, response) = await graphQl.QueryAsync(Context, request); if (hasError) { return BadRequest(response); } else { return Ok(response); } } /// /// Queries contents. /// /// The name of the app. /// The optional ids of the content to fetch. /// /// 200 => Contents returned. /// 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), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetAllContents(string app, [FromQuery] string ids) { var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids)); var response = Deferred.AsyncResponse(() => { return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); }); return Ok(response); } /// /// Queries contents. /// /// The name of the app. /// The required query object. /// /// 200 => Contents returned. /// 404 => App not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetAllContentsPost(string app, [FromBody] ContentsIdsQueryDto query) { var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(query.Ids)); var response = Deferred.AsyncResponse(() => { return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); }); return Ok(response); } /// /// Queries contents. /// /// The name of the app. /// The name of the schema. /// The optional ids of the content to fetch. /// The optional json query. /// /// 200 => Contents retunred. /// 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), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContents(string app, string name, [FromQuery] string? ids = null, [FromQuery] string? q = null) { var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); var contents = await contentQuery.QueryAsync(Context, name, CreateQuery(ids, q)); var response = Deferred.AsyncResponse(() => { return ContentsDto.FromContentsAsync(contents, Resources, schema, contentWorkflow); }); return Ok(response); } /// /// Queries contents. /// /// The name of the app. /// The name of the schema. /// The required query object. /// /// 200 => Contents returned. /// 404 => Schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/{name}/query")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContentsPost(string app, string name, [FromBody] QueryDto query) { var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); var contents = await contentQuery.QueryAsync(Context, name, query?.ToQuery() ?? Q.Empty); var response = Deferred.AsyncResponse(() => { return ContentsDto.FromContentsAsync(contents, Resources, schema, contentWorkflow); }); 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 returned. /// 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(ContentDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContent(string app, string name, DomainId id) { var content = await contentQuery.FindAsync(Context, name, id); if (content == null) { return NotFound(); } var response = ContentDto.FromContent(content, Resources); return Ok(response); } /// /// Get a content item validity. /// /// The name of the app. /// The name of the schema. /// The id of the content to fetch. /// /// 204 => Content is valid. /// 400 => Content not valid. /// 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}/validity")] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContentValidity(string app, string name, DomainId id) { var command = new ValidateContent { ContentId = id }; await CommandBus.PublishAsync(command); return NoContent(); } /// /// Get all references of a content. /// /// The name of the app. /// The name of the schema. /// The id of the content to fetch. /// The optional json query. /// /// 200 => Contents returned. /// 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}/references")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetReferences(string app, string name, DomainId id, [FromQuery] string? q = null) { var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReferencing(id)); var response = Deferred.AsyncResponse(() => { return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); }); return Ok(response); } /// /// Get a referencing contents of a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content to fetch. /// The optional json query. /// /// 200 => Content returned. /// 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}/referencing")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetReferencing(string app, string name, DomainId id, [FromQuery] string? q = null) { var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReference(id)); var response = Deferred.AsyncResponse(() => { return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); }); 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 version returned. /// 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}/{version}/")] [ApiPermissionOrAnonymous(Permissions.AppContentsRead)] [ApiCosts(1)] public async Task GetContentVersion(string app, string name, DomainId id, int version) { var content = await contentQuery.FindAsync(Context, name, id, version); if (content == null) { return NotFound(); } var response = ContentDto.FromContent(content, Resources); 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. /// True to automatically publish the content. /// The optional custom content id. /// /// 201 => Content created. /// 400 => Content request not valid. /// 404 => Content, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/{name}/")] [ProducesResponseType(typeof(ContentsDto), 201)] [ApiPermissionOrAnonymous(Permissions.AppContentsCreate)] [ApiCosts(1)] public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false, [FromQuery] DomainId? id = null) { var command = new CreateContent { Data = request.ToCleaned(), Publish = publish }; if (id != null && id.Value != default && !string.IsNullOrWhiteSpace(id.Value.ToString())) { command.ContentId = id.Value; } var response = await InvokeCommandAsync(command); return CreatedAtAction(nameof(GetContent), new { app, name, id = command.ContentId }, response); } /// /// Import content items. /// /// The name of the app. /// The name of the schema. /// The import request. /// /// 201 => Contents created. /// 400 => Content request not valid. /// 404 => Content references, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/{name}/import")] [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsCreate)] [ApiCosts(5)] public async Task PostContents(string app, string name, [FromBody] ImportContentsDto request) { var command = request.ToCommand(); var context = await CommandBus.PublishAsync(command); var result = context.Result(); var response = result.Select(x => BulkResultDto.FromImportResult(x, HttpContext)).ToArray(); return Ok(response); } /// /// Bulk update content items. /// /// The name of the app. /// The name of the schema. /// The bulk update request. /// /// 201 => Contents created. /// 400 => Content request not valid. /// 404 => Content references, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/{name}/bulk")] [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContents)] [ApiCosts(5)] public async Task BulkContents(string app, string name, [FromBody] BulkUpdateDto request) { var command = request.ToCommand(); var context = await CommandBus.PublishAsync(command); var result = context.Result(); var response = result.Select(x => BulkResultDto.FromImportResult(x, HttpContext)).ToArray(); return Ok(response); } /// /// Upsert a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to update. /// True to automatically publish the content. /// The full data for the content item. /// /// 200 => Content updated. /// 400 => Content request not valid. /// 404 => Content references, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/{name}/{id}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpsert)] [ApiCosts(1)] public async Task PostContent(string app, string name, DomainId id, [FromBody] NamedContentData request, [FromQuery] bool publish = false) { var command = new UpsertContent { ContentId = id, Data = request.ToCleaned(), Publish = publish }; var response = await InvokeCommandAsync(command); return Ok(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. /// /// 200 => Content updated. /// 400 => Content request not valid. /// 404 => Content references, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPut] [Route("content/{app}/{name}/{id}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PutContent(string app, string name, DomainId id, [FromBody] NamedContentData request) { var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() }; var response = await InvokeCommandAsync(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. /// /// 200 => Content patched. /// 400 => Content request not valid. /// 404 => Content, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPatch] [Route("content/{app}/{name}/{id}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PatchContent(string app, string name, DomainId id, [FromBody] NamedContentData request) { var command = new PatchContent { ContentId = id, Data = request.ToCleaned() }; var response = await InvokeCommandAsync(command); return Ok(response); } /// /// Change status of a content item. /// /// The name of the app. /// The name of the schema. /// The id of the content item to change. /// The status request. /// /// 200 => Content status changed. /// 400 => Content request not valid. /// 404 => Content, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPut] [Route("content/{app}/{name}/{id}/status/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PutContentStatus(string app, string name, DomainId id, ChangeStatusDto request) { var command = request.ToCommand(id); var response = await InvokeCommandAsync(command); return Ok(response); } /// /// Create a new draft version. /// /// The name of the app. /// The name of the schema. /// The id of the content item to create the draft for. /// /// 200 => Content draft created. /// 404 => Content, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/{name}/{id}/draft/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsVersionCreate)] [ApiCosts(1)] public async Task CreateDraft(string app, string name, DomainId id) { var command = new CreateContentDraft { ContentId = id }; var response = await InvokeCommandAsync(command); return Ok(response); } /// /// Delete the draft version. /// /// The name of the app. /// The name of the schema. /// The id of the content item to delete the draft from. /// /// 200 => Content draft deleted. /// 404 => Content, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpDelete] [Route("content/{app}/{name}/{id}/draft/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsDelete)] [ApiCosts(1)] public async Task DeleteVersion(string app, string name, DomainId id) { var command = new DeleteContentDraft { ContentId = id }; var response = await InvokeCommandAsync(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. /// True to check referrers of this content. /// /// 204 => Content deleted. /// 400 => Content cannot be 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}/")] [ApiPermissionOrAnonymous(Permissions.AppContentsDelete)] [ApiCosts(1)] public async Task DeleteContent(string app, string name, DomainId id, [FromQuery] bool checkReferrers = false) { var command = new DeleteContent { ContentId = id, CheckReferrers = checkReferrers }; await CommandBus.PublishAsync(command); return NoContent(); } private async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command); var result = context.Result(); var response = ContentDto.FromContent(result, Resources); return response; } private Q CreateQuery(string? ids, string? q) { return Q.Empty .WithIds(ids) .WithJsonQuery(q) .WithODataQuery(Request.QueryString.ToString()); } } }