Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content Delivery API #14051

Merged
merged 54 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5925fde
Add the core parts of the headless PoC
kjac Nov 30, 2022
e5e2013
Add Content API project (WIP - loads of TODOs and dupes that need to …
kjac Nov 30, 2022
a9a5cd7
Merge branch 'v12/dev' into v12/feature/headless
kjac Dec 1, 2022
ddb2722
Merge branch 'v12/dev' into v12/feature/headless
kjac Dec 2, 2022
5314e68
Rename the content API project and namespaces
kjac Dec 2, 2022
75327cb
Merge branch 'v12/dev' into v12/feature/headless
kjac Dec 21, 2022
7ec005a
Fixed bad merge
kjac Dec 21, 2022
cf72e07
Rename everything "Headless" to "ContentApi" or "Api"
kjac Dec 21, 2022
72c8add
Refactor Content + Media: Key => Id, Name not nullable
kjac Dec 21, 2022
539d878
Make Content API property return value types independent of datatype …
kjac Dec 21, 2022
e2153af
Clean up refactorings
kjac Dec 22, 2022
3f8f6ec
First stab at an expansion strategy using content picker as example i…
kjac Dec 23, 2022
d6caa7b
Merge branch 'v12/dev' into v12/feature/headless
kjac Jan 4, 2023
f47ff3c
Use named JSON options for content API serialization
kjac Jan 4, 2023
491ed16
Proper inclusion and registration of the content API
kjac Jan 4, 2023
a0ce781
Introduce API media builder
kjac Jan 4, 2023
8acb745
Make MNTP return API content/media depending on configuration (instea…
kjac Jan 4, 2023
9e8d350
Merge branch 'v12/dev' into v12/feature/headless
kjac Jan 24, 2023
7d4c436
Content API: Get by controllers (#13740)
elit0451 Jan 26, 2023
270e646
Content API: Add start-node header value to deal with url collisions …
elit0451 Feb 2, 2023
1ab908b
Merge branch 'v12/dev' into v12/feature/headless
kjac Feb 10, 2023
b93672d
Content API: Support Accept-Language header (#13831)
kjac Feb 16, 2023
3776521
Content API: Output expansion (#13848)
kjac Feb 17, 2023
40d51e3
Merge branch 'v12/dev' into v12/feature/headless
kjac Feb 23, 2023
fddd38c
Encapsulate content API dependencies in the DI
kjac Feb 23, 2023
4b38ef3
Support multi-site and multi-culture routing + a little rename/refact…
kjac Feb 24, 2023
3cd02ea
Rename services from "Default" to "Noop"
kjac Feb 27, 2023
cea294f
Ensure that Umbraco can boot without adding "AddContentApi()" to Conf…
kjac Feb 27, 2023
cac8698
Merge branch 'v12/dev' into v12/feature/headless
kjac Mar 4, 2023
dc90513
Moved incorrectly placed media builder
kjac Mar 4, 2023
6296e13
Fix API routes (#13915)
elit0451 Mar 6, 2023
6b7c37a
Fix multi URL picker value converter trying to access disposed object…
kjac Mar 17, 2023
cb5a4b0
Merge remote-tracking branch 'origin/v12/feature/headless' into v12/f…
kjac Mar 20, 2023
02fc748
Merge branch 'v12/dev' into v12/feature/headless
kjac Mar 20, 2023
a0292ae
Delivery API: Content routing and structure (#13984)
kjac Mar 20, 2023
78e1f74
Conditionally enabling the delivery API + add protection and preview …
kjac Mar 23, 2023
c6a2337
Include umbraco properties (width, height, ...) in the Media Properti…
kjac Mar 29, 2023
8af83d1
Add content type deny list (#14025)
kjac Mar 29, 2023
4f69b06
Dedicated property cache level for Content API (#14027)
kjac Mar 29, 2023
4826588
Merge branch 'v12/dev' into v12/feature/headless
kjac Mar 29, 2023
e818135
Support redirect tracking (#14033)
kjac Mar 30, 2023
05d3128
Add cultures and their routes to the API output (#14038)
kjac Mar 30, 2023
831e067
Delivery API: Query controller (#14041)
elit0451 Apr 6, 2023
c67963e
Merge branch 'v12/dev' into v12/feature/headless
kjac Apr 7, 2023
7c364fb
Delivery API: Adding pagination to query endpoint (#14083)
elit0451 Apr 14, 2023
6c0a977
Add missing CompatibilitySuppressions.xml
bergmania Apr 18, 2023
954806e
Merge remote-tracking branch 'origin/v12/dev' into v12/feature/headless
bergmania Apr 18, 2023
7eb2e11
Make Delivery API packable
bergmania Apr 18, 2023
9cc139d
Make Api.Common packable
bergmania Apr 18, 2023
cdcb2d0
Renamed extension method and namespace so it is discoverable
bergmania Apr 18, 2023
25233a2
Untangle ApiVersion configuration into api.common, so delivery api do…
bergmania Apr 18, 2023
e96eeab
configure options in management api
bergmania Apr 18, 2023
f0fef94
RTE output as JSON for Content API (#14067)
kjac Apr 18, 2023
bed9134
Rename to Delivery API (#14119)
kjac Apr 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Delivery API: Query controller (#14041)
* Initial commit

* Custom ContentAPIFieldDefinitionCollection

* Make index IUmbracoContentIndex

* Add querying for children by parent id (key)

* Add missing interface

* Adding querying endpoint

* Test code

* Compose unpublishedValueSet, so that you get the correct data in the ContentAPI index

* Renaming

* Fix ancestorKeys index values

* Adding IApiQueryExtensionService to be able to query the ContentAPI index in a generic way

* Fix IApiQueryService and clean up QueryContentApiController using it

* Support querying for path

* Fix content API indexing

* Fix default sorting

* Implement concrete QueryOption implementations

* Introduce new ExecuteQuery that uses the Core OptionHandlers

* Implement ExecuteQuery

* Change ExecuteQuery signature and implementation

* Implement demo sorting and fetching

* Add query option handlers and collection builder for them

* Cleanup

* Revert "Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992)"

This reverts commit 78e1f74.

* Revert "Delivery API: Content routing and structure (#13984)"

This reverts commit a0292ae.

* Revert "Fix multi URL picker value converter trying to access disposed objects in edge cases"

This reverts commit 6b7c37a.

* Revert "Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992)"

This reverts commit 78e1f74.

* Revert "Delivery API: Content routing and structure (#13984)"

This reverts commit a0292ae.

* Revert "Fix multi URL picker value converter trying to access disposed objects in edge cases"

This reverts commit 6b7c37a.

* Fix multi URL picker value converter trying to access disposed objects in edge cases

* Delivery API: Content routing and structure (#13984)

* Introduce content route instead of content path, rename and rework start item (previously start node) handling

* Strip out start node path in generated route path

* Make the start-item header take precedence over the request domain

* Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992)

* Test commit

* Refactored interfaces for the query handlers and for the selectors (that will handle the value of the fetch query option)

* Implemented a base class for the query options

* Refactored the names of the selectors and made use of the base class

* Refactored the ApiQueryService

* Refactored the QueryContentApiController.cs

* Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992)

* Fixing merge gone wrong

* Fix multi URL picker value converter trying to access disposed objects in edge cases

* Delivery API: Content routing and structure (#13984)

* Introduce content route instead of content path, rename and rework start item (previously start node) handling

* Strip out start node path in generated route path

* Make the start-item header take precedence over the request domain

* Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992)

* Make fetching work with the new setup

* Moving files to dedicated folders

* Removing ? for array

* Rename selector query method

* Implement FilterHandler and some filters

* Implement SortHandler and sort some sorts

* Refactoring

* Adding more fields to index due to querying

* Appending filtering and sorting queries

* Implementing a new ISelectorHandler without Examine types

* Re-implementing the collection to have a dedicated one for the selectors

* Implementing a new IFilterHandler without Examine types & refactoring the filters implementing it

* Adding a new collection dedicated to filters

* Renaming the old collection

* Implementing a new ISortHandler without Examine types & refactoring the sorts implementing it

* Adding a new collection for the sorts & adding all collections to UmbracoBuilder.Collections

* Refactoring the service to use the new collections and types

* Refactoring the fields in ContentApiFieldDefinitionCollection

* Remove nullability in Handlers

* Don't return null for selector

* Add TODO for having the filters support negation

* Changing the SortType to FieldType with our custom types on the SortOption

* Fix AncestorsSelector

* Fix ApiQueryService

* Documentation

* Fix Swagger docs

* Refactor the QueryContentApiController

* Adding handling for the IApiContentResponse in the JsonTypeResolver

* Refactor the service to use a safe fallback value in Examine queries

* Adding Noop for the IApiQueryService

* Cleanup

* Remove comment

* Fix name field for indexing

* Don't inherit QueryOptionBase in filters

* Fix casing for API index constant + swap FIXME with TODO

* Add TODO for handling missing fetch with start-item header

* Rename query handler parameters to not leak source (i.e. query string)

---------

Co-authored-by: kjac <[email protected]>
Co-authored-by: Elitsa <>
Co-authored-by: Zeegaan <[email protected]>
  • Loading branch information
3 people authored Apr 6, 2023
commit 831e067808e4d75b6de5bebe3488f01792afe6c5
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.ContentApi;
using Umbraco.Cms.Core.Models.ContentApi;
using Umbraco.Cms.Core.Models.PublishedContent;

namespace Umbraco.Cms.Api.Content.Controllers;
Expand All @@ -19,8 +20,7 @@ public ByIdContentApiController(IApiPublishedContentCache apiPublishedContentCac
/// <returns>The content item or not found result.</returns>
[HttpGet("item/{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IApiContentResponseBuilder), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ById(Guid id)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ public ByRouteContentApiController(
/// <returns>The content item or not found result.</returns>
[HttpGet("item/{*path}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IApiContentResponseBuilder), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByRoute(string path = "/")
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.ContentApi;
using Umbraco.Cms.Core.Models.ContentApi;
using Umbraco.Cms.Core.Models.PublishedContent;

namespace Umbraco.Cms.Api.Content.Controllers;

public class QueryContentApiController : ContentApiControllerBase
{
private readonly IApiQueryService _apiQueryService;

public QueryContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilderBuilder,
IApiQueryService apiQueryService)
: base(apiPublishedContentCache, apiContentResponseBuilderBuilder)
=> _apiQueryService = apiQueryService;

/// <summary>
/// Gets content item(s) from query.
/// </summary>
/// <param name="fetch">Optional fetch query parameter value.</param>
/// <param name="filter">Optional filter query parameters values.</param>
/// <param name="sort">Optional sort query parameters values.</param>
/// <returns>The content item(s) or empty collection.</returns>
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<IApiContentResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Query(string? fetch, [FromQuery] string[] filter, [FromQuery] string[] sort)
{
IEnumerable<Guid> ids = _apiQueryService.ExecuteQuery(fetch, filter, sort);
IEnumerable<IPublishedContent> contentItems = ApiPublishedContentCache.GetByIds(ids);
IEnumerable<IApiContentResponse> results = contentItems.Select(ApiContentResponseBuilder.Build);

return await Task.FromResult(Ok(results));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static IUmbracoBuilder AddContentApi(this IUmbracoBuilder builder)
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
builder.Services.AddSingleton<IApiQueryService, ApiQueryService>();

builder
.Services
Expand Down
24 changes: 14 additions & 10 deletions src/Umbraco.Cms.Api.Content/Json/ContentApiJsonTypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,22 @@ public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions option
{
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);

if (jsonTypeInfo.Type == typeof(IApiContent))
{
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
{
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
DerivedTypes =
{
new JsonDerivedType(typeof(ApiContent))
}
};
if (jsonTypeInfo.Type == typeof(IApiContent))
{
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContent));
}
else if (jsonTypeInfo.Type == typeof(IApiContentResponse))
{
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContentResponse));
}

return jsonTypeInfo;
}

private void ConfigureJsonPolymorphismOptions(JsonTypeInfo jsonTypeInfo, Type derivedType)
=> jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
{
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
DerivedTypes = { new JsonDerivedType(derivedType) }
};
}
38 changes: 38 additions & 0 deletions src/Umbraco.Cms.Api.Content/Querying/Filters/ContentTypeFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Umbraco.Cms.Core.ContentApi;

namespace Umbraco.Cms.Api.Content.Querying.Filters;

internal sealed class ContentTypeFilter : IFilterHandler
{
private const string ContentTypeSpecifier = "contentType:";

/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(ContentTypeSpecifier, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public FilterOption BuildFilterOption(string filter)
{
var alias = filter.Substring(ContentTypeSpecifier.Length);

var filterOption = new FilterOption
{
FieldName = "__NodeTypeAlias",
Value = string.Empty
};

// TODO: do we support negation?
if (alias.StartsWith('!'))
{
filterOption.Value = alias.Substring(1);
filterOption.Operator = FilterOperation.IsNot;
}
else
{
filterOption.Value = alias;
filterOption.Operator = FilterOperation.Is;
}

return filterOption;
}
}
38 changes: 38 additions & 0 deletions src/Umbraco.Cms.Api.Content/Querying/Filters/NameFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Umbraco.Cms.Core.ContentApi;

namespace Umbraco.Cms.Api.Content.Querying.Filters;

internal sealed class NameFilter : IFilterHandler
{
private const string NameSpecifier = "name:";

/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(NameSpecifier, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public FilterOption BuildFilterOption(string filter)
{
var value = filter.Substring(NameSpecifier.Length);

var filterOption = new FilterOption
{
FieldName = "name",
Value = string.Empty
};

// TODO: do we support negation?
if (value.StartsWith('!'))
{
filterOption.Value = value.Substring(1);
filterOption.Operator = FilterOperation.IsNot;
}
else
{
filterOption.Value = value;
filterOption.Operator = FilterOperation.Is;
}

return filterOption;
}
}
39 changes: 39 additions & 0 deletions src/Umbraco.Cms.Api.Content/Querying/QueryOptionBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using Umbraco.Cms.Core.ContentApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;

namespace Umbraco.Cms.Api.Content.Querying;

public abstract class QueryOptionBase
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IRequestRoutingService _requestRoutingService;

public QueryOptionBase(IPublishedSnapshotAccessor publishedSnapshotAccessor,
IRequestRoutingService requestRoutingService)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_requestRoutingService = requestRoutingService;
}

public Guid? GetGuidFromQuery(string queryStringValue)
{
if (Guid.TryParse(queryStringValue, out Guid id))
{
return id;
}

if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) ||
publishedSnapshot?.Content is null)
{
return null;
}

// Check if the passed value is a path of a content item
var contentRoute = _requestRoutingService.GetContentRoute(queryStringValue);
IPublishedContent? contentItem = publishedSnapshot.Content.GetByRoute(contentRoute);

return contentItem?.Key;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Umbraco.Cms.Core.ContentApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Content.Querying.Selectors;

internal sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
{
private const string AncestorsSpecifier = "ancestors:";
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;

public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService) =>
_publishedSnapshotAccessor = publishedSnapshotAccessor;

/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(AncestorsSpecifier, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public SelectorOption BuildSelectorOption(string selector)
{
var fieldValue = selector.Substring(AncestorsSpecifier.Length);
Guid? id = GetGuidFromQuery(fieldValue);

if (id is null ||
!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) ||
publishedSnapshot?.Content is null)
{
// Setting the Value to "" since that would yield no results.
// It won't be appropriate to return null here since if we reached this,
// it means that CanHandle() returned true, meaning that this Selector should be able to handle the selector value
return new SelectorOption
{
FieldName = "id",
Value = string.Empty
};
}

// With the previous check we made sure that if we reach this, we already made sure that there is a valid content item
IPublishedContent contentItem = publishedSnapshot.Content.GetById((Guid)id)!; // so it can't be null
IEnumerable<Guid> ancestorKeys = contentItem.Ancestors().Select(a => a.Key);

return new SelectorOption
{
FieldName = "id",
Value = string.Join(" ", ancestorKeys)
};
}
}
31 changes: 31 additions & 0 deletions src/Umbraco.Cms.Api.Content/Querying/Selectors/ChildrenSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Umbraco.Cms.Core.ContentApi;
using Umbraco.Cms.Core.PublishedCache;

namespace Umbraco.Cms.Api.Content.Querying.Selectors;

internal sealed class ChildrenSelector : QueryOptionBase, ISelectorHandler
{
private const string ChildrenSpecifier = "children:";

public ChildrenSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
}

/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(ChildrenSpecifier, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public SelectorOption BuildSelectorOption(string selector)
{
var fieldValue = selector.Substring(ChildrenSpecifier.Length);
Guid? id = GetGuidFromQuery(fieldValue);

return new SelectorOption
{
FieldName = "parentKey",
Value = id.ToString() ?? string.Empty
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Umbraco.Cms.Core.ContentApi;
using Umbraco.Cms.Core.PublishedCache;

namespace Umbraco.Cms.Api.Content.Querying.Selectors;

internal sealed class DescendantsSelector : QueryOptionBase, ISelectorHandler
{
private const string DescendantsSpecifier = "descendants:";

public DescendantsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor,
IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
}

/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(DescendantsSpecifier, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public SelectorOption BuildSelectorOption(string selector)
{
var fieldValue = selector.Substring(DescendantsSpecifier.Length);
Guid? id = GetGuidFromQuery(fieldValue);

return new SelectorOption
{
FieldName = "ancestorKeys",
Value = id.ToString() ?? string.Empty
};
}
}
26 changes: 26 additions & 0 deletions src/Umbraco.Cms.Api.Content/Querying/Sorts/LevelSort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.ContentApi;

namespace Umbraco.Cms.Api.Content.Querying.Sorts;

internal sealed class LevelSort : ISortHandler
{
private const string SortOptionSpecifier = "level:";

/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public SortOption BuildSortOption(string sort)
{
var sortDirection = sort.Substring(SortOptionSpecifier.Length);

return new SortOption
{
FieldName = "level",
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending,
FieldType = FieldType.Number
};
}
}
26 changes: 26 additions & 0 deletions src/Umbraco.Cms.Api.Content/Querying/Sorts/NameSort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.ContentApi;

namespace Umbraco.Cms.Api.Content.Querying.Sorts;

internal sealed class NameSort : ISortHandler
{
private const string SortOptionSpecifier = "name:";

/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public SortOption BuildSortOption(string sort)
{
var sortDirection = sort.Substring(SortOptionSpecifier.Length);

return new SortOption
{
FieldName = "name",
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending,
FieldType = FieldType.String
};
}
}
Loading