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

Expose contained part in GraphQL to allow retrieving the parent content item #17382

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using GraphQL.Types;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.GraphQL.Queries.Types;
using OrchardCore.Lists.Models;

namespace OrchardCore.Lists.GraphQL;

internal sealed class ContainedPartContentItemTypeInitializer : IContentItemTypeInitializer
{
internal IStringLocalizer S;
gvkries marked this conversation as resolved.
Show resolved Hide resolved

public ContainedPartContentItemTypeInitializer(IStringLocalizer<ContainedPartContentItemTypeInitializer> stringLocalizer)
{
S = stringLocalizer;
}

public void Initialize(ContentItemType contentItemType, ISchema schema)
{
foreach (var type in schema.AdditionalTypeInstances)
{
// Get all types with a list part that can contain the current type.
if (!type.Metadata.TryGetValue(nameof(ListPartSettings.ContainedContentTypes), out var containedTypes))
{
continue;
}

if ((containedTypes as IEnumerable<string>)?.Any(ct => ct == contentItemType.Name) != true)
{
continue;
}

var fieldType = schema.AdditionalTypeInstances.FirstOrDefault(t => t is ContainedQueryObjectType);

if (fieldType == null)
{
fieldType = ((IServiceProvider)schema).GetRequiredService<ContainedQueryObjectType>();
schema.RegisterType(fieldType);
}

contentItemType.Field<ContainedQueryObjectType>(type.Name.ToFieldName())
.Description(S["The parent content item of type {0}.", type.Name])
.Type(fieldType)
.Resolve(context =>
{
return context.Source.ContentItem.As<ContainedPart>();
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using GraphQL.Types;
using OrchardCore.ContentManagement.GraphQL.Queries.Types;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.Lists.Models;

namespace OrchardCore.Lists.GraphQL;

internal sealed class ContainedPartContentTypeBuilder : IContentTypeBuilder
{
public void Build(ISchema schema, FieldType contentQuery, ContentTypeDefinition contentTypeDefinition, ContentItemType contentItemType)
{
foreach (var listPart in contentTypeDefinition.Parts.Where(p => p.PartDefinition.Name.Equals(nameof(ListPart), StringComparison.OrdinalIgnoreCase)))
{
var settings = listPart?.GetSettings<ListPartSettings>();
if (settings == null)
{
continue;
}

if (contentItemType.Metadata.TryGetValue(nameof(ListPartSettings.ContainedContentTypes), out var containedContentTypes) &&
containedContentTypes is IEnumerable<string> existingContainedContentTypes)
{
contentItemType.Metadata[nameof(ListPartSettings.ContainedContentTypes)] = existingContainedContentTypes.Concat(settings.ContainedContentTypes).Distinct().ToArray();
}
else
{
contentItemType.Metadata[nameof(ListPartSettings.ContainedContentTypes)] = settings.ContainedContentTypes;
}
}
}

public void Clear()
{
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using GraphQL.Types;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using OrchardCore.Apis.GraphQL;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.GraphQL.Queries.Types;
using OrchardCore.Lists.Models;

namespace OrchardCore.Lists.GraphQL;
Expand All @@ -8,10 +12,25 @@ public class ContainedQueryObjectType : ObjectGraphType<ContainedPart>
{
public ContainedQueryObjectType(IStringLocalizer<ContainedQueryObjectType> S)
{
Name = "ContainedPart";
Description = S["Represents a link to the parent content item, and the order that content item is represented."];
Name = nameof(ContainedPart);
Description = S["Represents a link to the parent content item and the order in which the current content item is represented."];

Field(x => x.ListContentItemId);
Field(x => x.Order);

Field<ContentItemInterface, ContentItem>("listContentItem")
.Description(S["the parent list content item"])
.ResolveLockedAsync(async x =>
{
var contentItemId = x.Source.ListContentItemId;
var contentManager = x.RequestServices.GetService<IContentManager>();

return await contentManager.GetAsync(contentItemId);
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
});
gvkries marked this conversation as resolved.
Show resolved Hide resolved

Field(x => x.ListContentType)
.Description(S["the content type of the list owning the current content item."]);

Field(x => x.Order)
.Description(S["the order of the current content item in the list."]);
}
}
4 changes: 4 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Lists/GraphQL/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using OrchardCore.Apis;
using OrchardCore.ContentManagement.GraphQL;
using OrchardCore.ContentManagement.GraphQL.Queries;
using OrchardCore.ContentManagement.GraphQL.Queries.Types;
using OrchardCore.Lists.Indexes;
using OrchardCore.Lists.Models;
using OrchardCore.Modules;
Expand All @@ -18,5 +19,8 @@ public override void ConfigureServices(IServiceCollection services)
services.AddObjectGraphType<ListPart, ListQueryObjectType>();
services.AddTransient<IIndexAliasProvider, ContainedPartIndexAliasProvider>();
services.AddWhereInputIndexPropertyProvider<ContainedPartIndex>();

services.AddScoped<IContentTypeBuilder, ContainedPartContentTypeBuilder>();
services.AddTransient<IContentItemTypeInitializer, ContainedPartContentItemTypeInitializer>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,9 @@ public async Task BuildAsync(ISchema schema)
{
schema.Query.AddField(query);
}
else
{
// Register the content item type explicitly since it won't be discovered from the root 'query' type.
schema.RegisterType(typeType);
}

// Register the content item type explicitly to make it easier to find it.
schema.RegisterType(typeType);

if (!string.IsNullOrEmpty(stereotype))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,18 @@ private static async ValueTask<string> RenderShapeAsync(IResolveFieldContext<Con

return sw.ToString();
}

public override void Initialize(ISchema schema)
{
if (schema is IServiceProvider serviceProvider)
{
var initializers = serviceProvider.GetServices<IContentItemTypeInitializer>();
gvkries marked this conversation as resolved.
Show resolved Hide resolved
foreach (var initializer in initializers)
{
initializer.Initialize(this, schema);
}
}

base.Initialize(schema);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using GraphQL.Types;

namespace OrchardCore.ContentManagement.GraphQL.Queries.Types;

public interface IContentItemTypeInitializer
{
void Initialize(ContentItemType contentItemType, ISchema schema);
}
20 changes: 20 additions & 0 deletions src/docs/reference/modules/Apis.GraphQL.Abstractions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,26 @@ The following query gives us the results we want:
}
```

### Retrieve the parent content item using the List Part

When utilizing the List Part, you can also access the parent list content item. The following query demonstrates how to retrieve the parent blog content item for a blog post:

```json
{
blogPost {
blog {
listContentItem {
... on Blog {
displayText
}
}
}
}
}
```

This query will return the displayText of the parent blog content item associated with a specific blog post.

## More Info

For more information on GraphQL you can visit the following links:
Expand Down
16 changes: 16 additions & 0 deletions test/OrchardCore.Tests/Apis/GraphQL/Blog/BlogPostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,20 @@ public async Task ShouldNotReturnBlogsWithoutViewBlogContentPermission()

Assert.Equal(GraphQLApi.ValidationRules.RequiresPermissionValidationRule.ErrorCode, result["errors"][0]["extensions"]["number"].ToString());
}

[Fact]
public async Task ShouldQueryContainedPart()
{
using var context = new BlogContext();
await context.InitializeAsync();

var result = await context
.GraphQLClient
.Content
.Query("blogPost { blog { listContentItem { ... on Blog { displayText } } } }");

Assert.Equal(
"Blog",
result["data"]["blogPost"][0]["blog"]["listContentItem"]["displayText"].ToString());
}
}