Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"WebSearch",
"Bash(Remove-Item -Recurse -Force \"D:\\Projects\\Optimizely\\OptiGraphExtensions\\src\\OptiGraphExtensions\\Features\\StopWords\")",
"Bash(Remove-Item -Force \"D:\\Projects\\Optimizely\\OptiGraphExtensions\\src\\OptiGraphExtensions\\Entities\\StopWord.cs\")",
"Bash(Remove-Item -Recurse -Force \"D:\\Projects\\Optimizely\\OptiGraphExtensions\\src\\OptiGraphExtensions\\Views\\OptiGraphExtensions\\Administration\\StopWords\")"
"Bash(Remove-Item -Recurse -Force \"D:\\Projects\\Optimizely\\OptiGraphExtensions\\src\\OptiGraphExtensions\\Views\\OptiGraphExtensions\\Administration\\StopWords\")",
"Bash(dotnet restore:*)"
],
"deny": [],
"ask": []
Expand Down
13 changes: 13 additions & 0 deletions Sample/SampleCms/.config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.1",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}
12 changes: 12 additions & 0 deletions src/OptiGraphExtensions/Administration/AdministrationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ public IActionResult Webhooks()
return View("~/Views/OptiGraphExtensions/Administration/Webhooks/Index.cshtml", model);
}

[HttpGet]
[Route("~/optimizely-graphextensions/administration/query-library")]
public IActionResult QueryLibrary()
{
var model = new AdministrationViewModel
{
Title = "Opti Graph Extensions - Query Library",
};

return View("~/Views/OptiGraphExtensions/Administration/QueryLibrary/Index.cshtml", model);
}

[HttpGet]
[Route("~/optimizely-addon/administration/public/list")]
[AllowAnonymous]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public interface IOptiGraphExtensionsDataContext

DbSet<PinnedResult> PinnedResults { get; set; }

DbSet<SavedQuery> SavedQueries { get; set; }

Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ public OptiGraphExtensionsDataContext(
public DbSet<PinnedResultsCollection> PinnedResultsCollections { get; set; }

public DbSet<PinnedResult> PinnedResults { get; set; }

public DbSet<SavedQuery> SavedQueries { get; set; }
}
}
18 changes: 18 additions & 0 deletions src/OptiGraphExtensions/Entities/QueryType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace OptiGraphExtensions.Entities
{
/// <summary>
/// Represents the type of saved query for the Query Library.
/// </summary>
public enum QueryType
{
/// <summary>
/// Visual query built using the query builder UI.
/// </summary>
Visual = 0,

/// <summary>
/// Raw GraphQL query entered directly by the user.
/// </summary>
Raw = 1
}
}
100 changes: 100 additions & 0 deletions src/OptiGraphExtensions/Entities/SavedQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace OptiGraphExtensions.Entities
{
/// <summary>
/// Represents a saved query configuration for the Query Library.
/// Supports both visual query builder and raw GraphQL queries.
/// </summary>
[Table("tbl_OptiGraphExtensions_SavedQueries")]
public class SavedQuery
{
public Guid Id { get; set; }

[Required]
[StringLength(255)]
public string Name { get; set; } = string.Empty;

[StringLength(1000)]
public string? Description { get; set; }

/// <summary>
/// The type of query: Visual (0) or Raw (1).
/// </summary>
public QueryType QueryType { get; set; } = QueryType.Visual;

// === Visual Query Fields ===

/// <summary>
/// The content type to query (for Visual queries).
/// </summary>
[StringLength(255)]
public string? ContentType { get; set; }

/// <summary>
/// JSON-serialized List of selected field paths (for Visual queries).
/// Example: ["Name", "ContentLink.GuidValue", "Changed"]
/// </summary>
public string? SelectedFieldsJson { get; set; }

/// <summary>
/// JSON-serialized List of QueryFilter objects (for Visual queries).
/// </summary>
public string? FiltersJson { get; set; }

/// <summary>
/// Language filter for the query.
/// </summary>
[StringLength(10)]
public string? Language { get; set; }

/// <summary>
/// Field to sort results by (for Visual queries).
/// </summary>
[StringLength(100)]
public string? SortField { get; set; }

/// <summary>
/// Whether to sort in descending order.
/// </summary>
public bool SortDescending { get; set; }

// === Raw Query Fields ===

/// <summary>
/// The raw GraphQL query string (for Raw queries).
/// </summary>
public string? RawGraphQuery { get; set; }

/// <summary>
/// JSON-serialized Dictionary of query variables (for Raw queries).
/// Example: { "limit": 100, "locale": ["en"] }
/// </summary>
public string? QueryVariablesJson { get; set; }

// === Common Fields ===

/// <summary>
/// Number of items per page for pagination.
/// </summary>
public int PageSize { get; set; } = 100;

/// <summary>
/// Whether the query is active and visible to users.
/// </summary>
public bool IsActive { get; set; } = true;

// === Audit Fields ===

public DateTime CreatedAt { get; set; }

[StringLength(255)]
public string? CreatedBy { get; set; }

public DateTime? UpdatedAt { get; set; }

[StringLength(255)]
public string? UpdatedBy { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
using OptiGraphExtensions.Features.PinnedResults.Models;
using OptiGraphExtensions.Features.Webhooks.Services;
using OptiGraphExtensions.Features.Webhooks.Services.Abstractions;
using OptiGraphExtensions.Features.QueryLibrary.Repositories;
using OptiGraphExtensions.Features.QueryLibrary.Services;
using OptiGraphExtensions.Features.QueryLibrary.Services.Abstractions;

namespace OptiGraphExtensions.Features.Configuration;

Expand Down Expand Up @@ -75,6 +78,8 @@ public static IServiceCollection AddOptiGraphExtensions(
services.AddHttpClient<IPinnedResultsGraphSyncService, PinnedResultsGraphSyncService>();
services.AddHttpClient<IContentSearchService, ContentSearchService>();
services.AddHttpClient<IWebhookService, WebhookService>();
services.AddHttpClient<ISchemaDiscoveryService, SchemaDiscoveryService>();
services.AddHttpClient<IQueryExecutionService, QueryExecutionService>();

// Database
var connectionStringName = string.IsNullOrWhiteSpace(concreteOptions.ConnectionStringName) ? "EPiServerDB" : concreteOptions.ConnectionStringName;
Expand Down Expand Up @@ -177,5 +182,18 @@ internal static void SetUpOptiGraphExtensionsDatabase(this IServiceCollection se

// Register webhook services
services.AddScoped<IWebhookValidationService, WebhookValidationService>();

// Register Query Library services
services.AddScoped<SavedQueryRepository>();
services.AddScoped<ISavedQueryRepository>(provider =>
{
var baseRepository = provider.GetRequiredService<SavedQueryRepository>();
var cacheService = provider.GetRequiredService<ICacheService>();
return new CachedSavedQueryRepository(baseRepository, cacheService);
});
services.AddScoped<IQueryBuilderService, QueryBuilderService>();
services.AddScoped<IRawQueryService, RawQueryService>();
services.AddScoped<ICsvExportService, CsvExportService>();
services.AddScoped<ISavedQueryService, SavedQueryService>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using OptiGraphExtensions.Entities;

namespace OptiGraphExtensions.Features.QueryLibrary.Models
{
/// <summary>
/// Request model for executing a query.
/// </summary>
public class QueryExecutionRequest
{
/// <summary>
/// The type of query (Visual or Raw).
/// </summary>
public QueryType QueryType { get; set; } = QueryType.Visual;

// === Visual Query Fields ===

/// <summary>
/// The content type to query (for Visual queries).
/// </summary>
public string? ContentType { get; set; }

/// <summary>
/// List of field paths to select (for Visual queries).
/// </summary>
public List<string> SelectedFields { get; set; } = new();

/// <summary>
/// List of filter conditions (for Visual queries).
/// </summary>
public List<QueryFilter> Filters { get; set; } = new();

/// <summary>
/// Language filter.
/// </summary>
public string? Language { get; set; }

/// <summary>
/// Field to sort results by (for Visual queries).
/// </summary>
public string? SortField { get; set; }

/// <summary>
/// Whether to sort in descending order.
/// </summary>
public bool SortDescending { get; set; }

// === Raw Query Fields ===

/// <summary>
/// The raw GraphQL query string (for Raw queries).
/// </summary>
public string? RawGraphQuery { get; set; }

/// <summary>
/// Query variables as a dictionary (for Raw queries).
/// </summary>
public Dictionary<string, object>? QueryVariables { get; set; }

// === Common Fields ===

/// <summary>
/// Number of items per page.
/// </summary>
public int PageSize { get; set; } = 100;

/// <summary>
/// Cursor for pagination (null for first page).
/// </summary>
public string? Cursor { get; set; }
}

/// <summary>
/// Result model for query execution.
/// </summary>
public class QueryExecutionResult
{
/// <summary>
/// The data rows as a list of dictionaries (column name -> value).
/// </summary>
public List<Dictionary<string, object?>> Rows { get; set; } = new();

/// <summary>
/// The column names/headers in order.
/// </summary>
public List<string> Columns { get; set; } = new();

/// <summary>
/// Total count of items matching the query.
/// </summary>
public int TotalCount { get; set; }

/// <summary>
/// Cursor for the next page (null if no more pages).
/// </summary>
public string? NextCursor { get; set; }

/// <summary>
/// Whether there are more pages available.
/// </summary>
public bool HasMore { get; set; }

/// <summary>
/// Error message if the query failed.
/// </summary>
public string? ErrorMessage { get; set; }

/// <summary>
/// Whether the query execution was successful.
/// </summary>
public bool IsSuccess => string.IsNullOrEmpty(ErrorMessage);
}

/// <summary>
/// Result model for query validation.
/// </summary>
public class QueryValidationResult
{
/// <summary>
/// Whether the query is valid.
/// </summary>
public bool IsValid { get; set; }

/// <summary>
/// List of validation errors or warnings.
/// </summary>
public List<string> Messages { get; set; } = new();

/// <summary>
/// Whether the query supports pagination (has cursor and total fields).
/// </summary>
public bool SupportsPagination { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace OptiGraphExtensions.Features.QueryLibrary.Models
{
/// <summary>
/// Represents a filter condition for a visual query.
/// </summary>
public class QueryFilter
{
/// <summary>
/// The field name to filter on.
/// </summary>
public string Field { get; set; } = string.Empty;

/// <summary>
/// The filter operator (eq, neq, contains, startsWith, gt, lt, gte, lte).
/// </summary>
public string Operator { get; set; } = "eq";

/// <summary>
/// The value to filter by.
/// </summary>
public string Value { get; set; } = string.Empty;
}
}
Loading