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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Keep the docs focused on `CrestApps.Core`. If you need to mention the Orchard Co
- For optional provider integrations in sample hosts, do not eagerly read validated options in UI setup paths when an unconfigured provider should simply appear unavailable rather than crash the page.
- For catalog entry models, always provide an authoritative `CatalogEntryHandlerBase<T>` implementation that includes a `PopulateAsync` mapping path for every known property reachable from `JsonNode`/`JsonObject`, uses the shared JSON helper extensions instead of ad-hoc parsing where practical, sets create-time defaults (timestamps and current user/owner values when the model supports them) in `InitializedAsync`/`CreatingAsync`, and validates required fields in `ValidatingAsync`.
- For any `INameAwareModel` flow that has an authoritative catalog handler, validate duplicate names in the handler so users see a validation error early, but keep the store-level uniqueness enforcement as the final safeguard instead of moving that responsibility into managers.
- For catalog manager creation APIs, name-aware managers should offer both named and name-later `NewAsync` overloads, but any source-aware creation path must still require `source` up front and should not expose a name-only creation contract.
- When handler or service code needs to read typed values from `JsonNode`/`JsonObject`, add or reuse public helpers in `JsonNodeExtensions` rather than introducing new private `TryGetEnum`, `TryGetInt32`, `TryGetDateTime`, or similar parsing helpers in individual classes.
- Keep UI-only provider/authentication validation rules in the MVC and Blazor web projects instead of the framework handlers; for AI deployment and AI connection forms, enforce provider/connection/endpoint/API key requirements in the web layer so the handler layer stays focused on shared model concerns.
- Always keep exactly one trailing newline at the end of each file, no more and no less.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,13 @@ namespace CrestApps.Core.Services;

/// <summary>
/// A catalog manager that supports finding entries by their unique name,
/// extending <see cref="IReadCatalogManager{T}"/> with name-based lookup for models
/// extending <see cref="ICatalogManager{T}"/> with name-based lookup for models
/// that implement <see cref="INameAwareModel"/>.
/// </summary>
/// <typeparam name="T">The type of catalog entry, which must have a <see cref="INameAwareModel.Name"/> property.</typeparam>
public interface INamedCatalogManager<T> : IReadCatalogManager<T>
public interface INamedCatalogManager<T> : ICatalogManager<T>
where T : INameAwareModel
{
/// <summary>
/// Asynchronously deletes the specified model from the catalog.
/// </summary>
/// <param name="model">The model to delete.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns><see langword="true"/> if the model was successfully deleted; otherwise, <see langword="false"/>.</returns>
ValueTask<bool> DeleteAsync(T model, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously creates a new model instance pre-assigned to the specified name,
/// optionally populating it from JSON data.
Expand All @@ -30,29 +22,6 @@ public interface INamedCatalogManager<T> : IReadCatalogManager<T>
/// <returns>A newly created and initialized model instance assigned to the specified name.</returns>
ValueTask<T> NewAsync(string name, JsonNode data = null, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously creates the specified model in the catalog.
/// </summary>
/// <param name="model">The model to create.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
ValueTask CreateAsync(T model, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously updates the specified model in the catalog, optionally merging changes from JSON data.
/// </summary>
/// <param name="model">The model to update.</param>
/// <param name="data">Optional JSON data containing fields to merge into the model.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
ValueTask UpdateAsync(T model, JsonNode data = null, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously validates the specified model and returns the validation result.
/// </summary>
/// <param name="model">The model to validate.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The validation result details indicating success or failure with error messages.</returns>
ValueTask<ValidationResultDetails> ValidateAsync(T model, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously finds a catalog entry by its unique name.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,13 @@ namespace CrestApps.Core.Services;

/// <summary>
/// A catalog manager that supports composite lookup by both name and source,
/// extending <see cref="IReadCatalogManager{T}"/>
/// extending <see cref="ISourceCatalogManager{T}"/>
/// for models that implement both <see cref="INameAwareModel"/> and <see cref="ISourceAwareModel"/>.
/// </summary>
/// <typeparam name="T">The type of catalog entry.</typeparam>
public interface INamedSourceCatalogManager<T> : IReadCatalogManager<T>
public interface INamedSourceCatalogManager<T> : ISourceCatalogManager<T>
where T : INameAwareModel, ISourceAwareModel
{
/// <summary>
/// Asynchronously deletes the specified model from the catalog.
/// </summary>
/// <param name="model">The model to delete.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns><see langword="true"/> if the model was successfully deleted; otherwise, <see langword="false"/>.</returns>
ValueTask<bool> DeleteAsync(T model, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously creates a new model instance pre-assigned to the specified name and source,
/// optionally populating it from JSON data.
Expand All @@ -31,29 +23,6 @@ public interface INamedSourceCatalogManager<T> : IReadCatalogManager<T>
/// <returns>A newly created and initialized model instance assigned to the specified name and source.</returns>
ValueTask<T> NewAsync(string name, string source, JsonNode data = null, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously creates the specified model in the catalog.
/// </summary>
/// <param name="model">The model to create.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
ValueTask CreateAsync(T model, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously updates the specified model in the catalog, optionally merging changes from JSON data.
/// </summary>
/// <param name="model">The model to update.</param>
/// <param name="data">Optional JSON data containing fields to merge into the model.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
ValueTask UpdateAsync(T model, JsonNode data = null, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously validates the specified model and returns the validation result.
/// </summary>
/// <param name="model">The model to validate.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The validation result details indicating success or failure with error messages.</returns>
ValueTask<ValidationResultDetails> ValidateAsync(T model, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously finds a catalog entry by its unique name.
/// </summary>
Expand All @@ -62,22 +31,6 @@ public interface INamedSourceCatalogManager<T> : IReadCatalogManager<T>
/// <returns>The matching entry, or <see langword="null"/> if no entry with the specified name exists.</returns>
ValueTask<T> FindByNameAsync(string name, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously retrieves all catalog entries belonging to the specified source.
/// </summary>
/// <param name="source">The source or provider name to filter by.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>An enumerable of entries matching the specified source.</returns>
ValueTask<IEnumerable<T>> GetAsync(string source, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously finds all catalog entries that belong to the specified source.
/// </summary>
/// <param name="source">The source or provider name to search for.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>An enumerable of entries matching the specified source.</returns>
ValueTask<IEnumerable<T>> FindBySourceAsync(string source, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously retrieves a catalog entry by its unique name and source combination.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ description: Initial standalone release notes for the CrestApps.Core repository.
- expands Elasticsearch AI data source authentication beyond Basic by adding Elastic Cloud ID support plus `ApiKey`, `Base64ApiKey`, and `KeyIdAndKey` modes with protected per-source secrets in the MVC and Blazor editors and the shared Elasticsearch client factory
- adds `CrestApps.Core.PostgreSQL` and `CrestApps.Core.AI.PostgreSQL` packages providing a lightweight PostgreSQL + pgvector vector search backend as an alternative to Elasticsearch and Azure AI Search, registers the same keyed services (`ISearchIndexManager`, `ISearchDocumentManager`, `IDataSourceContentManager`, `IDataSourceDocumentReader`, `IODataFilterTranslator`) under the `"PostgreSQL"` provider name, supports `AddAIDocuments()`, `AddAIDataSources()`, and `AddAIMemory()` builder extensions, and integrates into both MVC and Blazor sample hosts
- fixes hosted document and data-source indexing flows so background workers create a scoped service provider before resolving scoped indexing services, preventing upload-triggered failures and similar nightly alignment lifetime issues
- aligns named catalog manager creation overloads so `INamedCatalogManager<T>` also exposes unnamed `NewAsync(...)` creation, while source-aware managers keep source-required creation paths and no longer advertise name-only manager registrations for source-bound AI templates and deployments
- standardizes Azure AI Search configuration on top-level `AuthenticationType`, `ApiKey`, `IdentityClientId`, and `IndexPrefix` settings under `CrestApps:AzureAISearch`, and refreshes the sample host / docs examples to list the full supported option set in one place
- adds an explicit Elasticsearch data-source environment selector (`SelfManaged` vs `CloudHosted`) so the MVC and Blazor editors show either `Url` or `CloudId` as appropriate and validation now requires the matching field for the chosen environment
- makes `AIDataSource` source-aware through the shared `Source` property, updates the AI data-source stores to expose `ISourceCatalog<AIDataSource>`, and removes the public `SourceType` model property in favor of `Source` while still reading legacy persisted `SourceType` payloads
Expand Down
2 changes: 1 addition & 1 deletion src/CrestApps.Core.Docs/docs/core/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Use these contracts when you are building reusable storage or management infrast
| --- | --- |
| `IReadCatalog<T>`, `ICatalog<T>` | Query and mutate catalog-backed data |
| `IReadCatalogManager<T>`, `ICatalogManager<T>` | Validation and lifecycle handling over catalogs |
| `INamedCatalog<T>`, `ISourceCatalog<T>`, related managers | Name-based and source-based lookup patterns |
| `INamedCatalog<T>`, `ISourceCatalog<T>`, related managers | Name-based and source-based lookup patterns, including name-optional creation for named managers and source-required creation for source-aware managers |
| `ICatalogEntryHandler<T>` | Hooks for create, update, and delete events |
| `INameAwareModel`, `IDisplayTextAwareModel`, `ISourceAwareModel` | Common model markers used across the framework |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ public static IServiceCollection AddCoreAITemplating(
});

services.TryAddScoped<IAIProfileTemplateManager, DefaultAIProfileTemplateManager>();
services.TryAddScoped<INamedCatalogManager<AIProfileTemplate>>(sp => (INamedCatalogManager<AIProfileTemplate>)sp.GetRequiredService<IAIProfileTemplateManager>());
services.TryAddScoped<ISourceCatalogManager<AIProfileTemplate>>(sp => (ISourceCatalogManager<AIProfileTemplate>)sp.GetRequiredService<IAIProfileTemplateManager>());
services.TryAddScoped<INamedSourceCatalogManager<AIProfileTemplate>>(sp => sp.GetRequiredService<IAIProfileTemplateManager>());
services.TryAddEnumerable(ServiceDescriptor.Scoped<ICatalogEntryHandler<AIProfileTemplate>, AIProfileTemplateCatalogHandler>());
Expand Down Expand Up @@ -443,10 +442,9 @@ public static IServiceCollection AddCoreAIOrchestration(this IServiceCollection

return snapshot.Value.ApplySiteOverrides(settings.CurrentValue);
});

// Register the Framework-level deployment manager.
services.TryAddScoped<IAIDeploymentManager, DefaultAIDeploymentManager>();
services.TryAddScoped<INamedCatalogManager<AIDeployment>>(sp => (INamedCatalogManager<AIDeployment>)sp.GetRequiredService<IAIDeploymentManager>());
services.TryAddScoped<IAIDeploymentManager, DefaultAIDeploymentManager>();
services.TryAddScoped<ISourceCatalogManager<AIDeployment>>(sp => (ISourceCatalogManager<AIDeployment>)sp.GetRequiredService<IAIDeploymentManager>());
services.TryAddScoped<INamedSourceCatalogManager<AIDeployment>>(sp => sp.GetRequiredService<IAIDeploymentManager>());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,28 @@ private async ValueTask<IEnumerable<AIProfile>> MergeProvidedProfilesAsync(
return merged;
}

/// <summary>
/// Creates a new AI profile instance.
/// </summary>
/// <param name="data">The optional initialization data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A newly created AI profile.</returns>
public new async ValueTask<AIProfile> NewAsync(JsonNode data = null, CancellationToken cancellationToken = default)
{
var profile = await base.NewAsync(data, cancellationToken);

EnsureDefaults(profile);

return profile;
}

/// <summary>
/// Creates a new AI profile instance.
/// </summary>
/// <param name="name">The profile name.</param>
/// <param name="data">The optional initialization data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A newly created AI profile assigned to the specified name.</returns>
public new async ValueTask<AIProfile> NewAsync(string name, JsonNode data = null, CancellationToken cancellationToken = default)
{
var profile = await base.NewAsync(name, data, cancellationToken);
Expand Down
15 changes: 14 additions & 1 deletion src/Primitives/CrestApps.Core/Services/NamedCatalogManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,24 @@ public async ValueTask<T> FindByNameAsync(string name, CancellationToken cancell
}

/// <summary>
/// News the operation.
/// Asynchronously creates a new model instance, optionally populating it from JSON data.
/// </summary>
/// <param name="data">Optional JSON data to seed the new model.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A newly created and initialized model instance.</returns>
public virtual async ValueTask<T> NewAsync(JsonNode? data = null, CancellationToken cancellationToken = default)
{
return await InitializeNewEntryAsync(new T(), data, cancellationToken);
}

/// <summary>
/// Asynchronously creates a new model instance pre-assigned to the specified name,
/// optionally populating it from JSON data.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A newly created and initialized model instance assigned to the specified name.</returns>
public virtual async ValueTask<T> NewAsync(string name, JsonNode? data = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(name);
Expand Down
Loading
Loading