// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== 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.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; using Squidex.Web.GraphQL; namespace Squidex.Areas.Api.Controllers.Contents { [SchemaMustBePublished] public sealed class ContentsController : ApiController { private readonly IContentQueryService contentQuery; private readonly IContentWorkflow contentWorkflow; private readonly GraphQLRunner graphQLRunner; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IContentWorkflow contentWorkflow, GraphQLRunner graphQLRunner) : base(commandBus) { this.contentQuery = contentQuery; this.contentWorkflow = contentWorkflow; this.graphQLRunner = graphQLRunner; } /// /// GraphQL endpoint. /// /// The name of the app. /// /// 200 => Contents returned or mutated. /// 404 => App not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpGet] [HttpPost] [Route("content/{app}/graphql/")] [Route("content/{app}/graphql/batch")] [ApiPermissionOrAnonymous] [ApiCosts(2)] public Task GetGraphQL(string app) { return graphQLRunner.InvokeAsync(HttpContext); } /// /// 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. /// [HttpGet] [Route("content/{app}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetAllContents(string app, AllContentsByGetDto query) { var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); 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] AllContentsByPostDto query) { var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); 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}/{schema}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContents(string app, string schema, [FromQuery] string? ids = null, [FromQuery] string? q = null) { var contents = await contentQuery.QueryAsync(Context, schema, CreateQuery(ids, q), HttpContext.RequestAborted); 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}/{schema}/query")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContentsPost(string app, string schema, [FromBody] QueryDto query) { var contents = await contentQuery.QueryAsync(Context, schema, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); 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}/{schema}/{id}/")] [ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContent(string app, string schema, DomainId id) { var content = await contentQuery.FindAsync(Context, schema, id, ct: HttpContext.RequestAborted); if (content == null) { return NotFound(); } var response = Deferred.Response(() => { return ContentDto.FromDomain(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}/{schema}/{id}/validity")] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContentValidity(string app, string schema, DomainId id) { var command = new ValidateContent { ContentId = id }; await CommandBus.PublishAsync(command, HttpContext.RequestAborted); 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}/{schema}/{id}/references")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetReferences(string app, string schema, DomainId id, [FromQuery] string? q = null) { var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReferencing(id), HttpContext.RequestAborted); 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}/{schema}/{id}/referencing")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetReferencing(string app, string schema, DomainId id, [FromQuery] string? q = null) { var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReference(id), HttpContext.RequestAborted); 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}/{schema}/{id}/{version}/")] [ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)] [ApiCosts(1)] public async Task GetContentVersion(string app, string schema, DomainId id, int version) { var content = await contentQuery.FindAsync(Context, schema, id, version, HttpContext.RequestAborted); if (content == null) { return NotFound(); } var response = ContentDto.FromDomain(content, Resources); return Ok(response.Data); } /// /// Create a content item. /// /// The name of the app. /// The name of the schema. /// The request parameters. /// /// 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}/{schema}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status201Created)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsCreate)] [ApiCosts(1)] public async Task PostContent(string app, string schema, CreateContentDto request) { var command = request.ToCommand(); var response = await InvokeCommandAsync(command); return CreatedAtAction(nameof(GetContent), new { app, schema, id = command.ContentId }, response); } /// /// Import content items. /// /// The name of the app. /// The name of the schema. /// The import request. /// /// 200 => 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}/{schema}/import")] [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsCreate)] [ApiCosts(5)] [Obsolete("Use bulk endpoint now.")] public async Task PostContents(string app, string schema, [FromBody] ImportContentsDto request) { var command = request.ToCommand(); var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = result.Select(x => BulkResultDto.FromDomain(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, update or delete. /// 400 => Contents request not valid. /// 404 => Contents references, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpPost] [Route("content/{app}/{schema}/bulk")] [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)] [ApiCosts(5)] public async Task BulkUpdateContents(string app, string schema, [FromBody] BulkUpdateContentsDto request) { var command = request.ToCommand(); var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = result.Select(x => BulkResultDto.FromDomain(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. /// The request parameters. /// /// 200 => Content created or 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}/{schema}/{id}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsUpsert)] [ApiCosts(1)] public async Task PostUpsertContent(string app, string schema, DomainId id, UpsertContentDto request) { var command = request.ToCommand(id); 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}/{schema}/{id}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsUpdateOwn)] [ApiCosts(1)] public async Task PutContent(string app, string schema, DomainId id, [FromBody] ContentData request) { var command = new UpdateContent { ContentId = id, Data = request }; 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}/{schema}/{id}/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsUpdateOwn)] [ApiCosts(1)] public async Task PatchContent(string app, string schema, DomainId id, [FromBody] ContentData request) { var command = new PatchContent { ContentId = id, Data = request }; 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}/{schema}/{id}/status/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsChangeStatusOwn)] [ApiCosts(1)] public async Task PutContentStatus(string app, string schema, DomainId id, [FromBody] ChangeStatusDto request) { var command = request.ToCommand(id); var response = await InvokeCommandAsync(command); return Ok(response); } /// /// Cancel status change of a content item. /// /// The name of the app. /// The name of the schema. /// The ID of the content item to cancel. /// /// 200 => Content status change cancelled. /// 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. /// [HttpDelete] [Route("content/{app}/{schema}/{id}/status/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsChangeStatusOwn)] [ApiCosts(1)] public async Task DeleteContentStatus(string app, string schema, DomainId id) { var command = new CancelContentSchedule { ContentId = 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}/{schema}/{id}/draft/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsVersionCreateOwn)] [ApiCosts(1)] public async Task CreateDraft(string app, string schema, 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}/{schema}/{id}/draft/")] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsVersionDeleteOwn)] [ApiCosts(1)] public async Task DeleteVersion(string app, string schema, 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. /// The request parameters. /// /// 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}/{schema}/{id}/")] [ApiPermissionOrAnonymous(PermissionIds.AppContentsDeleteOwn)] [ApiCosts(1)] public async Task DeleteContent(string app, string schema, DomainId id, DeleteContentDto request) { var command = request.ToCommand(id); await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } private async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = ContentDto.FromDomain(result, Resources); return response; } private Q CreateQuery(string? ids, string? q) { return Q.Empty .WithIds(ids) .WithJsonQuery(q) .WithODataQuery(Request.QueryString.ToString()); } } }