From b0244bf857013663754a56ff0647af81b6a8e501 Mon Sep 17 00:00:00 2001 From: Phil Whittaker Date: Fri, 14 Nov 2025 17:19:39 +0000 Subject: [PATCH] docs: Add CLAUDE.md documentation for key .NET projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive CLAUDE.md files for major Umbraco projects: - Root CLAUDE.md: Multi-project repository overview - Umbraco.Core: Interface contracts and domain models - Umbraco.Infrastructure: Implementation layer (NPoco, migrations, services) - Umbraco.Cms.Api.Common: Shared API infrastructure - Umbraco.Cms.Api.Management: Management API (1,317 files, 54 domains) - Umbraco.Web.UI.Client: Frontend with split docs structure Each file includes: - Architecture and design patterns - Project-specific workflows - Edge cases and gotchas - Commands and setup - Technical debt tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 339 ++++++++ src/Umbraco.Cms.Api.Common/CLAUDE.md | 382 +++++++++ src/Umbraco.Cms.Api.Management/CLAUDE.md | 622 ++++++++++++++ src/Umbraco.Core/CLAUDE.md | 518 ++++++++++++ src/Umbraco.Infrastructure/CLAUDE.md | 777 ++++++++++++++++++ src/Umbraco.Web.UI.Client/CLAUDE.md | 89 ++ .../docs/agentic-workflow.md | 457 ++++++++++ .../docs/architecture.md | 124 +++ src/Umbraco.Web.UI.Client/docs/clean-code.md | 397 +++++++++ src/Umbraco.Web.UI.Client/docs/commands.md | 214 +++++ src/Umbraco.Web.UI.Client/docs/edge-cases.md | 584 +++++++++++++ .../docs/error-handling.md | 242 ++++++ src/Umbraco.Web.UI.Client/docs/security.md | 299 +++++++ src/Umbraco.Web.UI.Client/docs/style-guide.md | 157 ++++ src/Umbraco.Web.UI.Client/docs/testing.md | 279 +++++++ 15 files changed, 5480 insertions(+) create mode 100644 CLAUDE.md create mode 100644 src/Umbraco.Cms.Api.Common/CLAUDE.md create mode 100644 src/Umbraco.Cms.Api.Management/CLAUDE.md create mode 100644 src/Umbraco.Core/CLAUDE.md create mode 100644 src/Umbraco.Infrastructure/CLAUDE.md create mode 100644 src/Umbraco.Web.UI.Client/CLAUDE.md create mode 100644 src/Umbraco.Web.UI.Client/docs/agentic-workflow.md create mode 100644 src/Umbraco.Web.UI.Client/docs/architecture.md create mode 100644 src/Umbraco.Web.UI.Client/docs/clean-code.md create mode 100644 src/Umbraco.Web.UI.Client/docs/commands.md create mode 100644 src/Umbraco.Web.UI.Client/docs/edge-cases.md create mode 100644 src/Umbraco.Web.UI.Client/docs/error-handling.md create mode 100644 src/Umbraco.Web.UI.Client/docs/security.md create mode 100644 src/Umbraco.Web.UI.Client/docs/style-guide.md create mode 100644 src/Umbraco.Web.UI.Client/docs/testing.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000000..f7976425a40e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,339 @@ +# Umbraco CMS - Multi-Project Repository + +Enterprise-grade headless CMS built on .NET 10.0. This repository contains 21 production projects organized in a layered architecture with clear separation of concerns. + +**Repository**: https://github.com/umbraco/Umbraco-CMS +**License**: MIT +**Main Branch**: `main` + +--- + +## 1. Overview + +### What This Repository Contains + +**21 Production Projects** organized in 3 main categories: + +1. **Core Architecture** (Domain & Infrastructure) + - `Umbraco.Core` - Interface contracts, domain models, notifications + - `Umbraco.Infrastructure` - Service implementations, data access, caching + +2. **Web & APIs** (Presentation Layer) + - `Umbraco.Web.UI` - Main ASP.NET Core web application + - `Umbraco.Web.Common` - Shared web functionality, controllers, middleware + - `Umbraco.Cms.Api.Management` - Backoffice Management API (REST) + - `Umbraco.Cms.Api.Delivery` - Content Delivery API (headless) + - `Umbraco.Cms.Api.Common` - Shared API infrastructure + +3. **Specialized Features** (Pluggable Modules) + - Persistence: EF Core (modern), NPoco (legacy) for SQL Server & SQLite + - Caching: `PublishedCache.HybridCache` (in-memory + distributed) + - Search: `Examine.Lucene` (full-text search) + - Imaging: `Imaging.ImageSharp` v1 & v2 (image processing) + - Other: Static assets, targets, development tools + +**6 Test Projects**: +- `Umbraco.Tests.Common` - Shared test utilities +- `Umbraco.Tests.UnitTests` - Unit tests +- `Umbraco.Tests.Integration` - Integration tests +- `Umbraco.Tests.Benchmarks` - Performance benchmarks +- `Umbraco.Tests.AcceptanceTest` - E2E tests +- `Umbraco.Tests.AcceptanceTest.UmbracoProject` - Test instance + +### Key Technologies + +- **.NET 10.0** - Target framework for all projects +- **ASP.NET Core** - Web framework +- **Entity Framework Core** - Modern ORM +- **OpenIddict** - OAuth 2.0/OpenID Connect authentication +- **Swashbuckle** - OpenAPI/Swagger documentation +- **Lucene.NET** - Full-text search via Examine +- **ImageSharp** - Image processing + +--- + +## 2. Repository Structure + +``` +Umbraco-CMS/ +├── src/ # 21 production projects +│ ├── Umbraco.Core/ # Domain contracts (interfaces only) +│ │ └── CLAUDE.md # ⭐ Core architecture guide +│ ├── Umbraco.Infrastructure/ # Service implementations +│ ├── Umbraco.Web.Common/ # Web utilities +│ ├── Umbraco.Web.UI/ # Main web application +│ ├── Umbraco.Cms.Api.Management/ # Management API +│ ├── Umbraco.Cms.Api.Delivery/ # Delivery API (headless) +│ ├── Umbraco.Cms.Api.Common/ # Shared API infrastructure +│ │ └── CLAUDE.md # ⭐ API patterns guide +│ ├── Umbraco.PublishedCache.HybridCache/ # Content caching +│ ├── Umbraco.Examine.Lucene/ # Search indexing +│ ├── Umbraco.Cms.Persistence.EFCore/ # EF Core data access +│ ├── Umbraco.Cms.Persistence.EFCore.Sqlite/ +│ ├── Umbraco.Cms.Persistence.EFCore.SqlServer/ +│ ├── Umbraco.Cms.Persistence.Sqlite/ # Legacy SQLite +│ ├── Umbraco.Cms.Persistence.SqlServer/ # Legacy SQL Server +│ ├── Umbraco.Cms.Imaging.ImageSharp/ # Image processing v1 +│ ├── Umbraco.Cms.Imaging.ImageSharp2/ # Image processing v2 +│ ├── Umbraco.Cms.StaticAssets/ # Embedded assets +│ ├── Umbraco.Cms.DevelopmentMode.Backoffice/ +│ ├── Umbraco.Cms.Targets/ # NuGet targets +│ └── Umbraco.Cms/ # Meta-package +│ +├── tests/ # 6 test projects +│ ├── Umbraco.Tests.Common/ +│ ├── Umbraco.Tests.UnitTests/ +│ ├── Umbraco.Tests.Integration/ +│ ├── Umbraco.Tests.Benchmarks/ +│ ├── Umbraco.Tests.AcceptanceTest/ +│ └── Umbraco.Tests.AcceptanceTest.UmbracoProject/ +│ +├── templates/ # Project templates +│ └── Umbraco.Templates/ +│ +├── tools/ # Build tools +│ └── Umbraco.JsonSchema/ +│ +├── umbraco.sln # Main solution file +├── Directory.Build.props # Shared build configuration +├── Directory.Packages.props # Centralized package versions +├── .editorconfig # Code style +└── .globalconfig # Roslyn analyzers +``` + +### Architecture Layers + +**Dependency Flow** (unidirectional, always flows inward): + +``` +Web.UI → Web.Common → Infrastructure → Core + ↓ + Api.Management → Api.Common → Infrastructure → Core + ↓ + Api.Delivery → Api.Common → Infrastructure → Core +``` + +**Key Principle**: Core has NO dependencies (pure contracts). Infrastructure implements Core. Web/APIs depend on Infrastructure. + +### Project Dependencies + +**Core Layer**: +- `Umbraco.Core` → No dependencies (only Microsoft.Extensions.*) + +**Infrastructure Layer**: +- `Umbraco.Infrastructure` → `Umbraco.Core` +- `Umbraco.PublishedCache.*` → `Umbraco.Infrastructure` +- `Umbraco.Examine.Lucene` → `Umbraco.Infrastructure` +- `Umbraco.Cms.Persistence.*` → `Umbraco.Infrastructure` + +**Web Layer**: +- `Umbraco.Web.Common` → `Umbraco.Infrastructure` + caching + search +- `Umbraco.Web.UI` → `Umbraco.Web.Common` + all features + +**API Layer**: +- `Umbraco.Cms.Api.Common` → `Umbraco.Web.Common` +- `Umbraco.Cms.Api.Management` → `Umbraco.Cms.Api.Common` +- `Umbraco.Cms.Api.Delivery` → `Umbraco.Cms.Api.Common` + +--- + +## 3. Teamwork & Collaboration + +### Branching Strategy + +- **Main branch**: `main` (protected) +- **Branch naming**: + - Features: `feature/description` or `contrib/username/description` + - Bug fixes: `bugfix/description` or `fix/issue-number` + - See `.github/CONTRIBUTING.md` for full guidelines + +### Pull Request Process + +- **PR Template**: `.github/pull_request_template.md` +- **Required CI Checks**: + - All tests pass + - Code formatting (dotnet format) + - No build warnings +- **Merge Strategy**: Squash and merge (via GitHub UI) +- **Reviews**: Required from code owners + +### Commit Messages + +Follow Conventional Commits format: +``` +(): + +Types: feat, fix, docs, style, refactor, test, chore +Scope: project name (core, web, api, etc.) + +Examples: +feat(core): add IContentService.GetByIds method +fix(api): resolve null reference in schema handler +docs(web): update routing documentation +``` + +### Code Owners + +Project ownership is distributed across teams. Check individual project directories for ownership. + +--- + +## 4. Architecture Patterns + +### Core Architectural Decisions + +1. **Layered Architecture with Dependency Inversion** + - Core defines contracts (interfaces) + - Infrastructure implements contracts + - Web/APIs consume implementations via DI + +2. **Interface-First Design** + - All services defined as interfaces in Core + - Enables testing, polymorphism, extensibility + +3. **Notification Pattern** (not C# events) + - `*SavingNotification` (before, cancellable) + - `*SavedNotification` (after, for reactions) + - Registered via Composers + +4. **Composer Pattern** (DI registration) + - Automatic discovery via reflection + - Ordered execution with `[ComposeBefore]`, `[ComposeAfter]`, `[Weight]` + +5. **Scoping Pattern** (Unit of Work) + - `ICoreScopeProvider.CreateCoreScope()` + - Must call `scope.Complete()` to commit + - Manages transactions and cache lifetime + +6. **Attempt Pattern** (operation results) + - `Attempt` instead of exceptions + - Strongly-typed operation status enums + +### Key Design Patterns Used + +- **Repository Pattern** - Data access abstraction +- **Unit of Work** - Scoping for transactions +- **Builder Pattern** - `ProblemDetailsBuilder` for API errors +- **Strategy Pattern** - OpenAPI handlers (schema ID, operation ID) +- **Options Pattern** - All configuration via `IOptions` +- **Factory Pattern** - Content type factories +- **Mediator Pattern** - Notification aggregator + +--- + +## 5. Project-Specific Notes + +### Centralized Package Management + +**All NuGet package versions** are centralized in `Directory.Packages.props`. Individual projects do NOT specify versions. + +```xml + + + + + +``` + +### Build Configuration + +- `Directory.Build.props` - Shared properties (target framework, company, copyright) +- `.editorconfig` - Code style rules +- `.globalconfig` - Roslyn analyzer rules + +### Migration from Legacy to EF Core + +The repository contains BOTH: +- **Legacy**: NPoco-based persistence (`Umbraco.Cms.Persistence.Sqlite`, `Umbraco.Cms.Persistence.SqlServer`) +- **Modern**: EF Core-based persistence (`Umbraco.Cms.Persistence.EFCore.*`) + +**Recommendation**: New development should use EF Core projects. + +### Authentication: OpenIddict + +All APIs use **OpenIddict** (OAuth 2.0/OpenID Connect): +- Reference tokens (not JWT) for better security +- ASP.NET Core Data Protection for token encryption +- Configured in `Umbraco.Cms.Api.Common` + +**Load Balancing Requirement**: All servers must share the same Data Protection key ring. + +### Content Caching Strategy + +**HybridCache** (`Umbraco.PublishedCache.HybridCache`): +- In-memory cache + distributed cache support +- Published content only (not draft) +- Invalidated via notifications and cache refreshers + +### API Versioning + +APIs use `Asp.Versioning.Mvc`: +- Management API: `/umbraco/management/api/v{version}/*` +- Delivery API: `/umbraco/delivery/api/v{version}/*` +- OpenAPI/Swagger docs per version + +### Known Limitations + +1. **Circular Dependencies**: Avoided via `Lazy` or event notifications +2. **Multi-Server**: Requires shared Data Protection key ring and synchronized clocks (NTP) +3. **Database Support**: SQL Server, SQLite (MySQL/PostgreSQL via community packages) + +--- + +## Quick Reference + +### Essential Commands + +```bash +# Build solution +dotnet build + +# Run all tests +dotnet test + +# Run specific test category +dotnet test --filter "Category=Integration" + +# Format code +dotnet format + +# Pack all projects +dotnet pack -c Release +``` + +### Key Projects + +| Project | Type | Description | +|---------|------|-------------| +| **Umbraco.Core** | Library | Interface contracts and domain models | +| **Umbraco.Infrastructure** | Library | Service implementations and data access | +| **Umbraco.Web.UI** | Application | Main web application (Razor/MVC) | +| **Umbraco.Cms.Api.Management** | Library | Management API (backoffice) | +| **Umbraco.Cms.Api.Delivery** | Library | Delivery API (headless CMS) | +| **Umbraco.Cms.Api.Common** | Library | Shared API infrastructure | +| **Umbraco.PublishedCache.HybridCache** | Library | Published content caching | +| **Umbraco.Examine.Lucene** | Library | Full-text search indexing | + +### Important Files + +- **Solution**: `umbraco.sln` +- **Build Config**: `Directory.Build.props`, `Directory.Packages.props` +- **Code Style**: `.editorconfig`, `.globalconfig` +- **Documentation**: `/CLAUDE.md`, `/src/Umbraco.Core/CLAUDE.md`, `/src/Umbraco.Cms.Api.Common/CLAUDE.md` + +### Project-Specific Documentation + +For detailed information about individual projects, see their CLAUDE.md files: +- **Core Architecture**: `/src/Umbraco.Core/CLAUDE.md` - Service contracts, notification patterns +- **API Infrastructure**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md` - OpenAPI, authentication, serialization + +### Getting Help + +- **Official Docs**: https://docs.umbraco.com/ +- **Contributing Guide**: `.github/CONTRIBUTING.md` +- **Issues**: https://github.com/umbraco/Umbraco-CMS/issues +- **Community**: https://our.umbraco.com/ + +--- + +**This repository follows a layered architecture with strict dependency rules. The Core defines contracts, Infrastructure implements them, and Web/APIs consume them. Each layer can be understood independently, but dependencies always flow inward toward Core.** diff --git a/src/Umbraco.Cms.Api.Common/CLAUDE.md b/src/Umbraco.Cms.Api.Common/CLAUDE.md new file mode 100644 index 000000000000..a40488c9fa7d --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/CLAUDE.md @@ -0,0 +1,382 @@ +# Umbraco.Cms.Api.Common + +Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery). + +--- + +## 1. Architecture + +**Type**: Class Library (NuGet Package) +**Target Framework**: .NET 10.0 +**Purpose**: Common API infrastructure - OpenAPI/Swagger, JSON serialization, OpenIddict authentication, problem details + +### Key Technologies + +- **ASP.NET Core** - Web framework +- **Swashbuckle** - OpenAPI/Swagger documentation generation +- **OpenIddict** - OAuth 2.0/OpenID Connect authentication +- **Asp.Versioning** - API versioning +- **System.Text.Json** - Polymorphic JSON serialization + +### Dependencies + +- `Umbraco.Core` - Domain models and service contracts +- `Umbraco.Web.Common` - Web functionality + +### Project Structure (45 files) + +``` +Umbraco.Cms.Api.Common/ +├── OpenApi/ # Schema/Operation ID handlers for Swagger +│ ├── SchemaIdHandler.cs # Generates schema IDs (e.g., "PagedUserModel") +│ ├── OperationIdHandler.cs # Generates operation IDs +│ └── SubTypesHandler.cs # Polymorphism support +├── Serialization/ # JSON type resolution +│ └── UmbracoJsonTypeInfoResolver.cs +├── Configuration/ # Options configuration +│ ├── ConfigureUmbracoSwaggerGenOptions.cs +│ └── ConfigureOpenIddict.cs +├── DependencyInjection/ # Service registration +│ ├── UmbracoBuilderApiExtensions.cs +│ └── UmbracoBuilderAuthExtensions.cs +├── Builders/ # RFC 7807 problem details +│ └── ProblemDetailsBuilder.cs +├── ViewModels/Pagination/ # Common DTOs +└── Security/ # Auth paths and handlers +``` + +### Design Patterns + +1. **Strategy Pattern** - `ISchemaIdHandler`, `IOperationIdHandler` (extensible via inheritance) +2. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses +3. **Options Pattern** - All configuration via `IConfigureOptions` + +--- + +## 2. Commands + +```bash +# Build +dotnet build src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj + +# Pack for NuGet +dotnet pack src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj -c Release + +# Run tests (integration tests in consuming APIs) +dotnet test tests/Umbraco.Tests.Integration/ + +# Check for outdated/vulnerable packages +dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --outdated +dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --vulnerable +``` + +--- + +## 3. Key Patterns + +### Virtual Handlers for Extensibility + +Handlers are intentionally virtual to allow consuming APIs to override: + +```csharp +// NOTE: Left unsealed on purpose, so it is extendable. +public class SchemaIdHandler : ISchemaIdHandler +{ + public virtual bool CanHandle(Type type) { } + public virtual string Handle(Type type) { } +} +``` + +**Why**: Management and Delivery APIs can customize schema/operation ID generation. + +### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:32) + +```csharp +// Remove invalid characters to prevent OpenAPI generation errors +return Regex.Replace(name, @"[^\w]", string.Empty); + +// Add "Model" suffix to avoid TypeScript name clashes (line 24) +if (name.EndsWith("Model") == false) +{ + name = $"{name}Model"; +} +``` + +### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:31-34) + +```csharp +// IMPORTANT: do NOT return an empty enumerable here. it will cause nullability to fail on reference +// properties, because "$ref" does not mix and match well with "nullable" in OpenAPI. +if (type.IsInterface is false) +{ + return new[] { type }; +} +``` + +**Why**: Interfaces must return concrete types to avoid OpenAPI schema conflicts. + +--- + +## 4. Testing + +**Location**: No direct tests - tested via integration tests in consuming APIs + +**How to test changes**: +```bash +# Run integration tests that exercise this library +dotnet test tests/Umbraco.Tests.Integration/ + +# Verify OpenAPI generation +# 1. Run Management API +# 2. Navigate to /umbraco/swagger/ +# 3. Check schema IDs and operation IDs +``` + +**Focus areas when testing**: +- OpenAPI document generation (schema IDs, operation IDs) +- Polymorphic JSON serialization/deserialization +- OpenIddict authentication flow +- Problem details formatting + +--- + +## 5. OpenIddict Authentication + +### Key Configuration (DependencyInjection/UmbracoBuilderAuthExtensions.cs) + +**Reference Tokens over JWT** (line 73-74): +```csharp +options + .UseReferenceAccessTokens() + .UseReferenceRefreshTokens(); +``` + +**Why**: More secure (revocable), better for load balancing, uses ASP.NET Core Data Protection. + +**Token Lifetime** (line 84-85): +```csharp +// Access token: 25% of refresh token lifetime +options.SetAccessTokenLifetime(new TimeSpan(timeOut.Ticks / 4)); +options.SetRefreshTokenLifetime(timeOut); +``` + +**PKCE Required** (line 54-56): +```csharp +options + .AllowAuthorizationCodeFlow() + .RequireProofKeyForCodeExchange(); +``` + +**Endpoints**: +- Backoffice: `/umbraco/management/api/v1/security/*` +- Member: `/umbraco/member/api/v1/security/*` + +--- + +## 6. Common Issues & Edge Cases + +### Polymorphic Deserialization Requires `$type` + +**Issue**: Deserializing to an interface without `$type` discriminator fails. + +**Handled in** (Json/NamedSystemTextJsonInputFormatter.cs:24-29): +```csharp +catch (NotSupportedException exception) +{ + // This happens when trying to deserialize to an interface, + // without sending the $type as part of the request + context.ModelState.TryAddModelException(string.Empty, + new InputFormatterException(exception.Message, exception)); +} +``` + +**Solution**: Clients must include `$type` property for interface types, or use concrete types. + +### Schema ID Collisions with TypeScript + +**Issue**: Type names like `Document` clash with TypeScript built-ins. + +**Solution** (OpenApi/SchemaIdHandler.cs:24-29): +```csharp +if (name.EndsWith("Model") == false) +{ + // Add "Model" postfix to all models + name = $"{name}Model"; +} +``` + +### Generic Type Handling + +**Issue**: `PagedViewModel` needs flattened schema name. + +**Solution** (OpenApi/SchemaIdHandler.cs:41-49): +```csharp +// Turns "PagedViewModel" into "PagedRelationItemModel" +return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}"; +``` + +--- + +## 7. Extending This Library + +### Adding a Custom OpenAPI Handler + +1. **Implement interface**: + ```csharp + public class MySchemaIdHandler : SchemaIdHandler + { + public override bool CanHandle(Type type) + => type.Namespace?.StartsWith("MyProject") is true; + + public override string Handle(Type type) + => $"My{base.Handle(type)}"; + } + ``` + +2. **Register in consuming API**: + ```csharp + builder.Services.AddSingleton(); + ``` + +**Note**: Handlers registered later take precedence in the selector. + +### Customizing Problem Details + +```csharp +var problemDetails = new ProblemDetailsBuilder() + .WithTitle("Validation Failed") + .WithDetail("The request contains errors") + .WithType("ValidationError") + .WithOperationStatus(MyOperationStatus.ValidationFailed) + .WithRequestModelErrors(errors) + .Build(); + +return BadRequest(problemDetails); +``` + +--- + +## 8. Project-Specific Notes + +### Why Reference Tokens Instead of JWT? + +**Decision**: Use `UseReferenceAccessTokens()` and ASP.NET Core Data Protection. + +**Tradeoffs**: +- ✅ **Pros**: Revocable, simpler key management, better security +- ❌ **Cons**: Requires database lookup (slower than JWT), needs shared Data Protection key ring + +**Load Balancing Requirement**: All servers must share the same Data Protection key ring and application name. + +### Why Virtual Handlers? + +**Decision**: Make `SchemaIdHandler`, `OperationIdHandler`, etc. virtual. + +**Why**: Management API and Delivery API have different schema ID requirements. Virtual methods allow override without rewriting the entire handler. + +**Example**: Management API might prefix all schemas with "Management", Delivery API with "Delivery". + +### Performance: Subtype Caching + +**Implementation** (Serialization/UmbracoJsonTypeInfoResolver.cs:14): +```csharp +private readonly ConcurrentDictionary> _subTypesCache = new(); +``` + +**Why**: Reflection is expensive. Cache discovered subtypes to avoid repeated `ITypeFinder.FindClassesOfType()` calls. + +### Known Limitations + +1. **Polymorphic Deserialization**: + - Requires `$type` discriminator in JSON for interfaces + - Only discovers types in Umbraco namespaces + - Not all .NET types are discoverable + +2. **OpenAPI Schema Generation**: + - Generic types are flattened (e.g., `PagedViewModel` → `PagedTModel`) + - Type names may need "Model" suffix to avoid clashes + +3. **OpenIddict Multi-Server**: + - Requires shared Data Protection key ring + - All servers must have synchronized clocks (NTP) + - Reference tokens require database storage + +### External Dependencies + +**OpenIddict**: +- OAuth 2.0 / OpenID Connect provider +- Version: See `Directory.Packages.props` +- Uses ASP.NET Core Data Protection for token encryption + +**Swashbuckle**: +- OpenAPI 3.0 document generation +- Custom filters: `EnumSchemaFilter`, `MimeTypeDocumentFilter`, `RemoveSecuritySchemesDocumentFilter` + +**Asp.Versioning**: +- API versioning via `ApiVersion` attribute +- API explorer integration for multi-version Swagger docs + +### Configuration + +**HTTPS** (Configuration/ConfigureOpenIddict.cs:14): +```csharp +// Disable transport security requirement for local development +options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false; +``` + +**⚠️ Warning**: Never disable HTTPS in production. + +### Usage by Consuming APIs + +**Registration Pattern**: +```csharp +// In Umbraco.Cms.Api.Management or Umbraco.Cms.Api.Delivery +builder + .AddUmbracoApiOpenApiUI() // Swagger + custom handlers + .AddUmbracoOpenIddict(); // OAuth 2.0 authentication +``` + +--- + +## Quick Reference + +### Essential Commands + +```bash +# Build +dotnet build src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj + +# Pack for NuGet +dotnet pack src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj -c Release + +# Test via integration tests +dotnet test tests/Umbraco.Tests.Integration/ +``` + +### Key Classes + +| Class | Purpose | File | +|-------|---------|------| +| `ProblemDetailsBuilder` | Build RFC 7807 error responses | Builders/ProblemDetailsBuilder.cs | +| `SchemaIdHandler` | Generate OpenAPI schema IDs | OpenApi/SchemaIdHandler.cs | +| `UmbracoJsonTypeInfoResolver` | Polymorphic JSON serialization | Serialization/UmbracoJsonTypeInfoResolver.cs | +| `UmbracoBuilderAuthExtensions` | Configure OpenIddict | DependencyInjection/UmbracoBuilderAuthExtensions.cs | +| `PagedViewModel` | Generic pagination model | ViewModels/Pagination/PagedViewModel.cs | + +### Important Files + +- `Umbraco.Cms.Api.Common.csproj` - Project dependencies +- `DependencyInjection/UmbracoBuilderApiExtensions.cs` - OpenAPI registration (line 12-30) +- `DependencyInjection/UmbracoBuilderAuthExtensions.cs` - OpenIddict setup (line 19-144) +- `Security/Paths.cs` - API endpoint path constants + +### Getting Help + +- **Root documentation**: `/CLAUDE.md` - Repository overview +- **Core patterns**: `/src/Umbraco.Core/CLAUDE.md` - Core contracts and patterns +- **Official docs**: https://docs.umbraco.com/ +- **OpenIddict docs**: https://documentation.openiddict.com/ + +--- + +**This library is the foundation for all Umbraco CMS REST APIs. Focus on OpenAPI customization, authentication configuration, and polymorphic serialization when working here.** diff --git a/src/Umbraco.Cms.Api.Management/CLAUDE.md b/src/Umbraco.Cms.Api.Management/CLAUDE.md new file mode 100644 index 000000000000..423a187a5a53 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/CLAUDE.md @@ -0,0 +1,622 @@ +# Umbraco CMS - Management API + +RESTful API for Umbraco backoffice operations. Manages content, media, users, and system configuration through OpenAPI-documented endpoints. + +**Project**: `Umbraco.Cms.Api.Management` +**Type**: ASP.NET Core Web API Library +**Files**: 1,317 C# files across 54+ controller domains + +--- + +## 1. Architecture + +### Target Framework +- **.NET 10.0** (`net10.0`) +- **C# 12** with nullable reference types enabled +- **ASP.NET Core** Web API + +### Application Type +**REST API Library** - Plugged into Umbraco.Web.UI, provides the Management API surface for backoffice operations. + +### Key Technologies +- **Web Framework**: ASP.NET Core MVC with `Asp.Versioning.Mvc` (v1.0 currently) +- **OpenAPI**: Swashbuckle.AspNetCore with custom schema/operation filters +- **Authentication**: OpenIddict via `Umbraco.Cms.Api.Common` (reference tokens, not JWT) +- **Authorization**: Policy-based with `IAuthorizationService` +- **Validation**: FluentValidation via base controllers +- **Serialization**: System.Text.Json with custom converters +- **Mapping**: Manual presentation factories (no AutoMapper) +- **Patching**: JsonPatch.Net for PATCH operations +- **Real-time**: SignalR hubs (`BackofficeHub`, `ServerEventHub`) +- **DI**: Microsoft.Extensions.DependencyInjection via `ManagementApiComposer` + +### Project Structure +``` +src/Umbraco.Cms.Api.Management/ +├── Controllers/ # 54+ domain-specific controller folders +│ ├── Document/ # Document (content) CRUD + publish/unpublish +│ ├── Media/ # Media CRUD + upload +│ ├── Member/ # Member management +│ ├── User/ # User management +│ ├── DataType/ # Data type configuration +│ ├── DocumentType/ # Content type schemas +│ ├── Template/ # Razor template management +│ ├── Dictionary/ # Localization dictionary +│ ├── Language/ # Language/culture config +│ ├── Security/ # Auth, login, external logins +│ ├── Install/ # Installation wizard +│ ├── Upgrade/ # Upgrade operations +│ ├── LogViewer/ # Log browsing +│ ├── HealthCheck/ # Health check dashboard +│ ├── Webhook/ # Webhook management +│ └── [48 more domains...] +│ +├── ViewModels/ # Request/response DTOs (one folder per domain) +├── Factories/ # Domain model → ViewModel converters +├── Services/ # Business logic (thin layer over Core.Services) +├── Mapping/ # ViewModel → domain model mappers +├── Security/ # Auth providers, sign-in manager, external logins +├── OpenApi/ # Swashbuckle filters (schema, operation, security) +├── Routing/ # Route configuration, SignalR hubs +├── DependencyInjection/ # Service registration (55+ files) +├── Middleware/ # Preview, server events +├── Configuration/ # IOptions configurators +├── Filters/ # Action filters +├── Serialization/ # JSON converters +└── OpenApi.json # Embedded OpenAPI spec (1.3MB) +``` + +### Dependencies +- **Umbraco.Cms.Api.Common** - Shared API infrastructure (base controllers, OpenAPI config) +- **Umbraco.Infrastructure** - Service implementations, data access +- **Umbraco.PublishedCache.HybridCache** - Published content queries +- **JsonPatch.Net** - JSON Patch (RFC 6902) support +- **Swashbuckle.AspNetCore** - OpenAPI generation + +### Design Patterns +1. **Controller-per-Operation** - Each endpoint is a separate controller class + - Example: `CreateDocumentController`, `UpdateDocumentController`, `DeleteDocumentController` + - Enables fine-grained authorization and operation-specific logic + +2. **Presentation Factory Pattern** - Factories convert domain models to ViewModels + - Example: `IDocumentEditingPresentationFactory` (src/Umbraco.Cms.Api.Management/Factories/) + - Separation: Controllers → Factories → ViewModels + +3. **Attempt Pattern** - Operations return `Attempt` for status-based error handling + - Controllers map status enums to HTTP status codes via helper methods + +4. **Authorization Service Pattern** - All authorization via `IAuthorizationService`, not attributes + - Checked in base controller methods (see `ManagementApiControllerBase`) + +5. **Options Pattern** - All configuration via `IOptions` (security, routing, OpenAPI) + +6. **SignalR Event Broadcasting** - Real-time notifications via `BackofficeHub` and `ServerEventHub` + +--- + +## 2. Commands + +### Build & Run +```bash +# Build +dotnet build src/Umbraco.Cms.Api.Management + +# Test (tests in ../../tests/Umbraco.Tests.Integration and Umbraco.Tests.UnitTests) +dotnet test --filter "FullyQualifiedName~Management" + +# Pack (for NuGet distribution) +dotnet pack src/Umbraco.Cms.Api.Management -c Release +``` + +### Code Quality +```bash +# Format code +dotnet format src/Umbraco.Cms.Api.Management + +# Build with warnings (note: some warnings suppressed, see .csproj line 23) +dotnet build src/Umbraco.Cms.Api.Management /p:TreatWarningsAsErrors=true +``` + +### OpenAPI Documentation +The project embeds a pre-generated `OpenApi.json` (1.3MB). To regenerate: +```bash +# Run Umbraco.Web.UI, access /umbraco/swagger +# Export JSON from Swagger UI +``` + +### Package Management +```bash +# Add package (versions centralized in Directory.Packages.props) +dotnet add src/Umbraco.Cms.Api.Management package [PackageName] + +# Check for vulnerable packages +dotnet list src/Umbraco.Cms.Api.Management package --vulnerable +``` + +### Environment Setup +1. **Prerequisites**: .NET 10 SDK +2. **IDE**: Visual Studio 2022 or Rider (with .editorconfig support) +3. **Configuration**: Inherits from `Umbraco.Web.UI` appsettings (no app settings in this library) + +--- + +## 3. Style Guide + +### Project-Specific Patterns + +**Controller Naming** (line examples from CreateDocumentController.cs:16): +```csharp +[ApiVersion("1.0")] +public class CreateDocumentController : CreateDocumentControllerBase +``` +- Pattern: `{Verb}{Entity}Controller` (e.g., `CreateDocumentController`, `UpdateMediaController`) +- Base class: `{Verb}{Entity}ControllerBase` for shared logic +- **Critical**: One operation per controller (not one controller per resource) + +**Async Naming** - All async methods use `Async` suffix consistently: +```csharp +await _contentEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor)); +``` + +**Factory Pattern Usage** (line 44): +```csharp +ContentCreateModel model = _documentEditingPresentationFactory.MapCreateModel(requestModel); +``` +- ViewModels → Domain: `Map{Operation}Model(requestModel)` +- Domain → ViewModels: Factory classes in `Factories/` folder + +### Key Patterns from Codebase + +**ControllerBase Helper Methods** (inherited from `ManagementApiControllerBase` in Api.Common): +- `CreatedAtId(expression, id)` - Returns 201 with Location header +- `ContentEditingOperationStatusResult(status)` - Maps status enum to ProblemDetails +- `CurrentUserKey(accessor)` - Gets current user from security context + +**Authorization Pattern** (all controllers): +```csharp +private readonly IAuthorizationService _authorizationService; +// Check permissions in action, not via [Authorize] attribute +``` + +--- + +## 4. Test Bench + +### Test Location +- **Unit Tests**: `tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/` +- **Integration Tests**: `tests/Umbraco.Tests.Integration/Umbraco.Cms.Api.Management/` + +### Running Tests +```bash +# All Management API tests +dotnet test --filter "FullyQualifiedName~Management" + +# Specific domain (e.g., Document controllers) +dotnet test --filter "FullyQualifiedName~Management.Controllers.Document" +``` + +### Testing Focus +1. **Controller logic** - Request validation, authorization checks, status code mapping +2. **Factories** - ViewModel ↔ Domain model conversion accuracy +3. **Authorization** - Policy enforcement for each operation +4. **OpenAPI schema** - Ensure Swagger generation doesn't break + +### InternalsVisibleTo +Tests have access to internal types (see .csproj:44-52): +- `Umbraco.Tests.UnitTests` +- `Umbraco.Tests.Integration` +- `DynamicProxyGenAssembly2` (for Moq) + +--- + +## 5. Error Handling + +### Operation Status Pattern +Controllers use `Attempt` with strongly-typed status enums: +```csharp +Attempt result = + await _contentEditingService.CreateAsync(model, userKey); + +return result.Success + ? CreatedAtId(controller => nameof(controller.ByKey), result.Result.Content!.Key) + : ContentEditingOperationStatusResult(result.Status); // Maps to ProblemDetails +``` + +**Status Enums** (from Core): +- `ContentEditingOperationStatus` - InvalidParent, NotFound, NotAllowed, etc. +- `UserOperationStatus` - UserNameIsNotEmail, DuplicateUserName, etc. +- Each enum value maps to specific HTTP status + ProblemDetails type + +### ProblemDetails +All errors return RFC 7807 ProblemDetails via helper methods in base controllers: +- 400 Bad Request: Validation failures, invalid operations +- 403 Forbidden: Authorization failures +- 404 Not Found: Resource not found +- 409 Conflict: Duplicate operations + +### Critical Logging Points +1. **Authorization failures** - Logged by AuthorizationService +2. **Service operation failures** - Logged in Infrastructure layer services +3. **External login errors** - BackOfficeSignInManager (Security/) + +--- + +## 6. Clean Code + +### Key Design Decisions + +**Why Controller-per-Operation?** (not RESTful resource-based controllers) +- Fine-grained authorization per operation +- Operation-specific request/response models +- Clearer OpenAPI documentation +- Example: 20+ controllers in `Controllers/Document/` for different operations + +**Why Manual Factories instead of AutoMapper?** +- Explicit control over mapping logic +- Easier debugging (no magic) +- Better performance (no reflection overhead) +- See `Factories/` directory (92 factory classes) + +**Why JsonPatch.Net?** +- RFC 6902 compliant JSON Patch support +- Used for partial updates (PATCH operations) +- See `ViewModels/JsonPatch/JsonPatchViewModel.cs` + +### Project-Specific Architectural Decisions + +**Embedded OpenAPI Spec** (OpenApi.json - 1.3MB): +- Pre-generated, embedded as resource +- Served for client SDK generation +- **Why?** Deterministic output, faster startup (no runtime generation) + +**SignalR for Real-time** (Routing/BackofficeHub.cs:33): +- `BackofficeHub` - User notifications, cache refreshes +- `ServerEventHub` - Background job updates, health checks +- Routes: `/umbraco/backoffice-signalr`, `/umbraco/serverevent-signalr` + +### Code Smells to Watch For + +1. **Large factory classes** - Some factories have 1000+ lines (e.g., `UserGroupPresentationFactory.cs`) + - Consider splitting by operation + +2. **Repeated authorization checks** - Each controller duplicates auth logic + - Already abstracted to base classes, but still verbose + +3. **ViewModel explosion** - 1000+ ViewModel classes across ViewModels/ folders + - Consider shared base models or composition + +--- + +## 7. Security + +### Authentication & Authorization +**Method**: OpenIddict (OAuth 2.0) via Umbraco.Cms.Api.Common +- Reference tokens (not JWT) stored in database +- Token validation via OpenIddict middleware +- ASP.NET Core Data Protection for token encryption + +**Authorization**: +```csharp +// Example from CreateDocumentController.cs:22 +private readonly IAuthorizationService _authorizationService; + +// Authorization checked in base controller methods, not attributes +protected async Task HandleRequest(request, Func> handler) +{ + var authResult = await _authorizationService.AuthorizeAsync(User, request, policy); + // ... +} +``` + +**Policies** (defined in Security/Authorization/): +- `ContentPermissionHandler` - Document/Media CRUD permissions +- `SectionAccessHandler` - Backoffice section access +- `UserGroupPermissionHandler` - Admin operations + +**Password Requirements** (Security/ConfigureBackOfficeIdentityOptions.cs:18): +- See Identity options configuration + +### External Login Providers +**Location**: `Security/BackOfficeExternalLoginProviders.cs` +- Google, Microsoft, OpenID Connect providers +- Auto-linking with `ExternalSignInAutoLinkOptions` +- **Critical**: Validate external claims before auto-linking users + +### Input Validation +**FluentValidation** used throughout: +- Request models validated automatically via MVC integration +- Custom validators in each domain folder (e.g., `ViewModels/Document/Validators/`) + +**Parameter Validation**: +```csharp +// Controllers validate IDs, keys before service calls +if (requestModel.Parent == null) + return BadRequest(new ProblemDetailsBuilder()...); +``` + +### Data Access Security +**SQL Injection Prevention**: +- All data access via EF Core (parameterized queries) +- No raw SQL in this project + +### API Security +**CORS** - Configured in Umbraco.Web.UI (not this project) + +**HTTPS Enforcement** - Configured in Umbraco.Web.UI + +**Request Size Limits** - Configured for file uploads in Umbraco.Web.UI + +**Security Headers** - Handled by Umbraco.Web.UI middleware + +### Secrets Management +**No secrets in this project** - Configuration injected from parent application (Umbraco.Web.UI) + +### Dependency Security +```bash +# Check vulnerable packages +dotnet list src/Umbraco.Cms.Api.Management package --vulnerable +``` + +### Security Anti-Patterns to Avoid +1. **Never bypass authorization checks** - All operations must authorize +2. **Never trust client validation** - Always validate on server +3. **Never expose stack traces** - ProblemDetails abstracts errors +4. **Never log sensitive data** - User passwords, tokens, API keys + +--- + +## 8. Teamwork and Workflow + +**⚠️ SKIPPED** - This is a sub-project. See root `/CLAUDE.md` for repository-wide teamwork protocols. + +--- + +## 9. Edge Cases + +### Domain-Specific Edge Cases + +**Document Operations**: +1. **Publishing with descendants** - Can timeout on large trees + - Use `PublishDocumentWithDescendantsController` with result polling + - See `Controllers/Document/PublishDocumentWithDescendantsResultController.cs` + +2. **Recycle bin operations** - Items in recycle bin can't be published + - Check `IsTrashed` before publish operations + +3. **Public access rules** - Affects authorization and routing + - See `Controllers/Document/CreatePublicAccessDocumentController.cs` + +**Media Upload**: +1. **Large file uploads** - Request size limits in parent app + - Controllers accept multipart/form-data + - Temporary files cleaned by background job + +2. **Media picker** - Can reference deleted media + - Validation in `Factories/` checks for orphaned references + +**User Management**: +1. **External logins** - Auto-linking can create duplicate users if email mismatches + - See `Security/ExternalSignInAutoLinkOptions.cs` + +2. **User groups** - Deleting user group doesn't delete users + - Users reassigned to default group + +**Webhooks**: +1. **Webhook failures** - Failed webhooks retry with exponential backoff + - See `Controllers/Webhook/` for configuration + +### Known Gotchas (from TODO comments) + +**StyleSheet/Script/PartialView Tree Controllers** - All have identical TODO comment: +``` +TODO: [NL] This must return path segments for a query to work +// src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs +// src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs +// src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs +``` + +**BackOfficeController** - External login TODO: +``` +// src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs +// TODO: Handle external logins properly +``` + +--- + +## 10. Agentic Workflow + +### When to Add a New Endpoint + +**Decision Points**: +1. Does the operation fit an existing controller domain? (Document, Media, User, etc.) +2. Is this a new CRUD operation or a custom action? +3. Does this require new authorization policies? + +**Workflow**: +1. **Create Controller** in appropriate `Controllers/{Domain}/` folder + - Follow naming: `{Verb}{Entity}Controller` + - Inherit from domain-specific base controller or `ManagementApiControllerBase` + +2. **Define ViewModels** in `ViewModels/{Domain}/` + - Request model (e.g., `CreateDocumentRequestModel`) + - Response model (if not reusing existing) + +3. **Create or Update Factory** in `Factories/` + - Map request ViewModel → domain model + - Map domain result → response ViewModel + +4. **Authorization** - Check required permissions in controller action + - Use `IAuthorizationService.AuthorizeAsync()` + +5. **Service Layer** - Call Core services (from `Umbraco.Core.Services`) + - Handle `Attempt<>` results + - Map status to HTTP status codes + +6. **OpenAPI Annotations**: + - `[ApiVersion("1.0")]` + - `[MapToApiVersion("1.0")]` + - `[ProducesResponseType(...)]` for all status codes + +7. **Testing**: + - Unit test controller logic + - Integration test end-to-end flow + +8. **Update OpenApi.json** (if needed for client generation) + +### Quality Gates Before PR +1. All tests pass +2. Code formatted (`dotnet format`) +3. No new warnings (check suppressed warnings list in .csproj:23) +4. OpenAPI schema valid (run Swagger UI) +5. Authorization tested (unit + integration tests) + +### Common Pitfalls +1. **Forgetting authorization checks** - Every operation must authorize +2. **Inconsistent status code mapping** - Use base controller helpers +3. **Large factory classes** - Split by operation if >500 lines +4. **Missing ProducesResponseType** - Breaks OpenAPI client generation +5. **Not handling Attempt failures** - Always check `result.Success` + +--- + +## 11. Project-Specific Notes + +### Key Design Decisions + +**Why 1,317 files for one API?** +- Controller-per-operation pattern = many controllers +- 54 domains × average 10-20 operations per domain +- Tradeoff: Verbose but explicit, easier to navigate than megacontrollers + +**Why manual factories instead of AutoMapper?** +- Performance: No reflection overhead +- Debuggability: Step through mapping logic +- Control: Complex mappings (e.g., security trimming) require custom logic + +**Why embedded OpenApi.json?** +- Deterministic OpenAPI spec for client generation +- Faster startup (no runtime generation) +- Easier versioning (commit changes to spec) + +### External Integrations +None directly in this project. All integrations handled by: +- **Umbraco.Infrastructure** - External search, media, email providers +- **Umbraco.Core** - External data sources, webhooks + +### Known Limitations + +1. **API Versioning**: Currently only v1.0 + - Future versions will require new controller classes or action methods + +2. **Batch Operations**: Limited batch endpoint support + - Most operations are single-entity (create one document at a time) + +3. **Real-time Limits**: SignalR hubs don't scale beyond single-server without Redis backplane + - Configure Redis for multi-server setups + +4. **File Upload Size**: Controlled by parent app (Umbraco.Web.UI) + - This project doesn't set limits + +### Performance Considerations + +**Caching**: +- Published content cached via `Umbraco.PublishedCache.HybridCache` +- Controllers query cache, not database (for published content) + +**Background Jobs**: +- Long-running operations (publish with descendants, export) return job ID +- Poll result endpoint for completion +- See `Controllers/Document/PublishDocumentWithDescendantsResultController.cs` + +**OpenAPI Generation**: +- Pre-generated (OpenApi.json embedded) +- Runtime generation disabled for performance + +### Technical Debt (Top Issues from TODO comments) + +1. **Warnings Suppressed** (Umbraco.Cms.Api.Management.csproj:9-22): + ``` + TODO: Fix and remove overrides: + - SA1117: params all on same line + - SA1401: make fields private + - SA1134: own line attributes + - CS0108: hidden inherited member + - CS0618/CS9042: update obsolete references + - CS1998: remove async or make method synchronous + - CS8524: switch statement exhaustiveness + - IDE0060: removed unused parameter + - SA1649: file name match type + - CS0419: ambiguous reference + - CS1573: param tag for all parameters + - CS1574: unresolveable cref + ``` + +2. **Tree Controller Path Segments** (multiple files): + - Stylesheet/Script/PartialView tree controllers need path segment support for queries + - Files: `Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs` + - Files: `Controllers/Script/Tree/ScriptTreeControllerBase.cs` + - Files: `Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs` + +3. **External Login Handling** (Controllers/Security/BackOfficeController.cs): + - TODO: Handle external logins properly + +4. **Large Factory Classes** (Factories/UserGroupPresentationFactory.cs): + - Some factories exceed 1000 lines - consider splitting + +5. **ViewModel Explosion** - 1000+ ViewModel classes + - Consider shared base models or composition to reduce duplication + +--- + +## Quick Reference + +### Essential Commands +```bash +# Build +dotnet build src/Umbraco.Cms.Api.Management + +# Test Management API +dotnet test --filter "FullyQualifiedName~Management" + +# Format code +dotnet format src/Umbraco.Cms.Api.Management + +# Pack for NuGet +dotnet pack src/Umbraco.Cms.Api.Management -c Release +``` + +### Key Projects +- **Umbraco.Cms.Api.Management** (this) - Management API controllers and models +- **Umbraco.Cms.Api.Common** - Shared API infrastructure, base controllers +- **Umbraco.Infrastructure** - Service implementations, data access +- **Umbraco.Core** - Domain models, service interfaces + +### Important Files +- **Project file**: `src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj` +- **Composer**: `src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs` (DI entry point) +- **Routing**: `src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs` +- **OpenAPI Spec**: `src/Umbraco.Cms.Api.Management/OpenApi.json` (embedded, 1.3MB) +- **Base Controllers**: See `Umbraco.Cms.Api.Common` project + +### Configuration +No appsettings in this library - all configuration from parent app (Umbraco.Web.UI): +- OpenIddict settings +- CORS settings +- File upload limits + +### API Endpoints +Base path: `/umbraco/management/api/v1/` + +Examples: +- `POST /umbraco/management/api/v1/document` - Create document +- `GET /umbraco/management/api/v1/document/{id}` - Get document by key +- `PUT /umbraco/management/api/v1/document/{id}` - Update document +- `DELETE /umbraco/management/api/v1/document/{id}` - Delete document + +Full spec: See OpenApi.json or Swagger UI at `/umbraco/swagger` + +### Getting Help +- **Root Documentation**: `/CLAUDE.md` (repository overview) +- **API Common Docs**: `../Umbraco.Cms.Api.Common/CLAUDE.md` (shared API patterns) +- **Core Docs**: `../Umbraco.Core/CLAUDE.md` (domain architecture) +- **Official Docs**: https://docs.umbraco.com/umbraco-cms/reference/management-api diff --git a/src/Umbraco.Core/CLAUDE.md b/src/Umbraco.Core/CLAUDE.md new file mode 100644 index 000000000000..da4544218112 --- /dev/null +++ b/src/Umbraco.Core/CLAUDE.md @@ -0,0 +1,518 @@ +# Umbraco.Core Project Guide + +## Project Overview + +**Umbraco.Core** is the foundational layer of Umbraco CMS, containing the core domain models, services interfaces, events/notifications system, and essential abstractions. This project has NO database implementation and minimal external dependencies - it focuses on defining contracts and business logic. + +**Package ID**: `Umbraco.Cms.Core` +**Namespace**: `Umbraco.Cms.Core` + +### What Lives Here vs. Other Projects + +- **Umbraco.Core**: Interfaces, models, notifications, service contracts, core business logic, configuration models +- **Umbraco.Infrastructure**: Concrete implementations (repositories, database access, file systems, concrete services) +- **Umbraco.Web.Common**: Web-specific functionality (controllers, middleware, routing) +- **Umbraco.Cms.Api.Common**: API endpoints and DTOs + +## Key Directory Structure + +### Core Domain Areas + +``` +/Models - Domain models and DTOs +├── /Entities - Base entity interfaces (IEntity, IUmbracoEntity, ITreeEntity) +├── /ContentEditing - Content editing models and DTOs +├── /ContentPublishing - Publishing-related models +├── /Membership - User, member, and group models +├── /PublishedContent - Published content abstractions +├── /DeliveryApi - Delivery API models +└── /Blocks - Block List/Grid models + +/Services - Service interfaces (~237 files!) +├── /OperationStatus - Enums for service operation results (48 status types) +├── /ContentTypeEditing - Content type editing services +├── /Navigation - Navigation services +└── /ImportExport - Import/export services + +/Notifications - Event notification system (100+ notification types) +/Events - Event infrastructure and handlers +``` + +### Infrastructure & Patterns + +``` +/Composing - Composer pattern for DI registration +/DependencyInjection - IUmbracoBuilder and DI extensions +/Scoping - Unit of work pattern (ICoreScopeProvider) +/Cache - Caching abstractions and refreshers +├── /Refreshers - Cache invalidation for distributed systems +└── /NotificationHandlers - Cache invalidation via notifications + +/PropertyEditors - Property editor abstractions +├── /ValueConverters - Convert stored values to typed values +├── /Validation - Property validation infrastructure +└── /DeliveryApi - Delivery API property converters + +/Persistence - Repository interfaces (no implementations!) +├── /Repositories - Repository contracts +└── /Querying - Query abstractions +``` + +### Supporting Functionality + +``` +/Configuration - Configuration models and settings +├── /Models - Strongly-typed settings (50+ configuration classes) +└── /UmbracoSettings - Legacy Umbraco settings + +/Extensions - Extension methods (100+ extension files) +/Mapping - Object mapping abstractions (IUmbracoMapper) +/Serialization - JSON serialization configuration +/Security - Security abstractions and authorization +/Routing - URL routing abstractions +/Templates - Template/view abstractions +/Webhooks - Webhook event system +/HealthChecks - Health check abstractions +/Telemetry - Telemetry/analytics +/Install & /Installer - Installation infrastructure +``` + +## Core Patterns and Conventions + +### 1. Service Layer Pattern + +Services follow a consistent structure: + +```csharp +// Interface defines contract (in Umbraco.Core) +public interface IContentService +{ + IContent? GetById(Guid key); + Task> CreateAsync(...); +} + +// Implementation lives in Umbraco.Infrastructure +// Service operations return Attempt for typed results +``` + +**Key conventions**: +- Interfaces in Umbraco.Core, implementations in Umbraco.Infrastructure +- Use `Attempt` for operations that can fail with specific reasons +- OperationStatus enums provide detailed failure reasons +- Services are registered in Composers via DI + +### 2. Notification System (Event Handling) + +Umbraco uses a notification pattern instead of traditional events: + +```csharp +// 1. Define notification (implements INotification) +public class ContentSavedNotification : INotification +{ + public IEnumerable SavedEntities { get; } +} + +// 2. Create handler +public class MyNotificationHandler : INotificationHandler +{ + public void Handle(ContentSavedNotification notification) + { + // React to content being saved + } +} + +// 3. Register in composer +builder.AddNotificationHandler(); +``` + +**Notification types**: +- `*SavingNotification` - Before save (cancellable via `ICancelableNotification`) +- `*SavedNotification` - After save +- `*DeletingNotification` / `*DeletedNotification` +- `*MovingNotification` / `*MovedNotification` + +**Key interface**: `IEventAggregator` - publishes notifications to handlers + +### 3. Composer Pattern (DI Registration) + +Composers register services and configure the application: + +```csharp +public class MyComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + // Register services + builder.Services.AddSingleton(); + + // Add to collections + builder.PropertyEditors().Add(); + + // Register notification handlers + builder.AddNotificationHandler(); + } +} +``` + +**Composer ordering**: +- `[ComposeBefore(typeof(OtherComposer))]` +- `[ComposeAfter(typeof(OtherComposer))]` +- `[Weight(100)]` - lower runs first + +### 4. Entity and Content Model Hierarchy + +``` +IEntity - Base: Id, Key, CreateDate, UpdateDate + └─ IUmbracoEntity - Adds: Name, CreatorId, ParentId, Path, Level, SortOrder + └─ ITreeEntity - Tree structure support + └─ IContentBase - Adds: Properties, ContentType, CultureInfos + ├─ IContent - Documents (publishable) + ├─ IMedia - Media items + └─ IMember - Members +``` + +**Key interfaces**: +- `IContentBase` - Common base for all content items (properties, cultures) +- `IContent` - Documents with publishing workflow +- `IContentType`, `IMediaType`, `IMemberType` - Define structure +- `IProperty` - Individual property on content +- `IPropertyType` - Property definition on content type + +### 5. Scoping Pattern (Unit of Work) + +```csharp +public class MyService +{ + private readonly ICoreScopeProvider _scopeProvider; + + public void DoWork() + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + + // Do database work + // Access repositories + + scope.Complete(); // Commit transaction + } +} +``` + +**Key points**: +- Scopes manage transactions and cache lifetime +- Must call `scope.Complete()` to commit +- Scopes can be nested (innermost controls transaction) +- `RepositoryCacheMode` controls caching behavior + +### 6. Property Editors + +Property editors define how data is edited and stored: + +```csharp +public class MyPropertyEditor : IDataEditor +{ + public string Alias => "My.PropertyEditor"; + + public IDataValueEditor GetValueEditor() + { + return new MyDataValueEditor(); + } + + public IConfigurationEditor GetConfigurationEditor() + { + return new MyConfigurationEditor(); + } +} +``` + +**Key interfaces**: +- `IDataEditor` - Property editor registration +- `IDataValueEditor` - Value editing and conversion +- `IPropertyIndexValueFactory` - Search indexing +- `IPropertyValueConverter` - Convert stored values to typed values + +### 7. Cache Refreshers (Distributed Cache) + +For multi-server deployments, cache refreshers synchronize cache: + +```csharp +public class MyEntityCacheRefresher : CacheRefresherBase +{ + public override Guid RefresherUniqueId => new Guid("..."); + public override string Name => "My Entity Cache Refresher"; + + public override void RefreshAll() + { + // Clear all cache + } + + public override void Refresh(int id) + { + // Clear cache for specific entity + } +} +``` + +## Important Base Classes and Interfaces + +### Must-Know Abstractions + +#### Service Layer +- `IContentService` - Content CRUD operations +- `IContentTypeService` - Content type management +- `IMediaService` - Media operations +- `IDataTypeService` - Data type configuration +- `IUserService` - User management +- `ILocalizationService` - Languages and dictionary +- `IRelationService` - Entity relationships + +#### Content Models +- `IContent` / `IContentBase` - Document entities +- `IContentType` - Document type definition +- `IProperty` / `IPropertyType` - Property definitions +- `IPublishedContent` - Read-only published content + +#### Infrastructure +- `ICoreScopeProvider` - Unit of work / transactions +- `IEventAggregator` - Notification publishing +- `IUmbracoMapper` - Object mapping +- `IComposer` / `IUmbracoBuilder` - DI registration + +#### Entity Base Classes +- `EntityBase` - Basic entity with Id, Key, dates +- `TreeEntityBase` - Adds Name, hierarchy +- `BeingDirty` / `ICanBeDirty` - Change tracking + +## Key Files and Constants + +### Essential Files + +1. **Constants.cs** (and 40 Constants-*.cs files) + - `Constants.System.*` - System-level constants + - `Constants.Security.*` - Security and authorization + - `Constants.Conventions.*` - Naming conventions + - `Constants.PropertyEditors.*` - Built-in property editor aliases + - `Constants.ObjectTypes.*` - Entity type GUIDs + - Use these instead of magic strings! + +2. **Udi.cs** / **GuidUdi.cs** / **StringUdi.cs** + - Umbraco Identifiers (like URIs): `umb://document/{guid}` + - Used throughout for entity references + - `UdiParser.Parse("umb://document/...")` to parse + +3. **Attempt.cs** and **Attempt.cs** + - Result pattern for operations that can fail + - `Attempt.Succeed(value)` / `Attempt.Fail()` + - `Attempt` - typed result with status + +### Configuration + +Configuration models in `/Configuration/Models`: +- `ContentSettings` - Content-related settings +- `GlobalSettings` - Global Umbraco settings +- `SecuritySettings` - Security configuration +- `DeliveryApiSettings` - Delivery API configuration +- Access via `IOptionsMonitor` + +## Dependencies + +### What Umbraco.Core Depends On +- Microsoft.Extensions.* - DI, Configuration, Logging, Caching, Options +- Microsoft.Extensions.Identity.Core - Identity infrastructure +- NO database dependencies +- NO web dependencies + +### What Depends on Umbraco.Core +- **Umbraco.Infrastructure** - Implements all Core interfaces +- **Umbraco.PublishedCache.HybridCache** - Published content caching +- **Umbraco.Cms.Persistence.EFCore** - EF Core persistence +- **Umbraco.Cms.Api.Common** - API infrastructure +- All higher-level Umbraco projects + +## Common Development Tasks + +### 1. Creating a New Service + +```csharp +// 1. Define interface in Umbraco.Core/Services/IMyService.cs +public interface IMyService +{ + Task> CreateAsync(...); +} + +// 2. Define operation status in Services/OperationStatus/ +public enum MyOperationStatus +{ + Success, + NotFound, + ValidationFailed +} + +// 3. Implement in Umbraco.Infrastructure +// 4. Register in a Composer +builder.Services.AddScoped(); +``` + +### 2. Adding a Notification Handler + +```csharp +// 1. Identify notification (e.g., ContentSavingNotification) +// 2. Create handler +public class MyContentHandler : INotificationHandler +{ + public void Handle(ContentSavingNotification notification) + { + foreach (var content in notification.SavedEntities) + { + // Validate, modify, or react + } + + // Cancel if needed (for cancellable notifications) + // notification.Cancel = true; + } +} + +// 3. Register in Composer +builder.AddNotificationHandler(); +``` + +### 3. Creating a Property Editor + +```csharp +// 1. Define in Umbraco.Core/PropertyEditors/ +[DataEditor("My.Alias", "My Editor", "view")] +public class MyPropertyEditor : DataEditor +{ + public MyPropertyEditor(IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) + { } + + protected override IDataValueEditor CreateValueEditor() + { + return DataValueEditorFactory.Create(Attribute!); + } +} + +// 2. Register in Composer +builder.PropertyEditors().Add(); +``` + +### 4. Working with Content + +```csharp +public class MyService +{ + private readonly IContentService _contentService; + private readonly ICoreScopeProvider _scopeProvider; + + public async Task UpdateContentAsync(Guid key) + { + using var scope = _scopeProvider.CreateCoreScope(); + + IContent? content = _contentService.GetById(key); + if (content == null) + return; + + // Modify content + content.SetValue("propertyAlias", "new value"); + + // Save (triggers notifications) + var result = _contentService.Save(content); + + scope.Complete(); + } +} +``` + +### 5. Extending Content Types + +```csharp +public class ContentTypeCustomizationComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.AddNotificationHandler(); + } +} + +public class MyHandler : INotificationHandler +{ + public void Handle(ContentTypeSavedNotification notification) + { + foreach (var contentType in notification.SavedEntities) + { + // React to content type changes + } + } +} +``` + +### 6. Defining Configuration + +```csharp +// 1. Create settings class in Configuration/Models/ +public class MySettings +{ + public string ApiKey { get; set; } = string.Empty; + public int MaxItems { get; set; } = 10; +} + +// 2. Bind in Composer +builder.Services.Configure( + builder.Config.GetSection("Umbraco:MySettings")); + +// 3. Inject via IOptionsMonitor +``` + +## Testing Considerations + +The project has `InternalsVisibleTo` attributes for: +- `Umbraco.Tests` +- `Umbraco.Tests.Common` +- `Umbraco.Tests.UnitTests` +- `Umbraco.Tests.Integration` +- `Umbraco.Tests.Benchmarks` + +Internal types are accessible in test projects for more thorough testing. + +## Architecture Principles + +1. **Separation of Concerns**: Core defines contracts, Infrastructure implements them +2. **Interface-First**: Always define interfaces before implementations +3. **Notification Pattern**: Use notifications instead of events for extensibility +4. **Attempt Pattern**: Return typed results with operation status +5. **Composer Pattern**: Use composers for DI registration and configuration +6. **Scoping**: Use scopes for unit of work and transaction management +7. **No Direct Database Access**: Core has no repositories implementations +8. **Culture Variance**: Full support for multi-language content +9. **Extensibility**: Everything is designed to be extended or replaced + +## Common Gotchas + +1. **Don't implement repositories in Core** - They belong in Infrastructure +2. **Always use scopes** - Database operations require a scope +3. **Complete scopes** - Forgot `scope.Complete()`? Changes won't save +4. **Notification timing** - *Saving notifications are cancellable, *Saved are not +5. **Service locator** - Don't use `IServiceProvider` directly, use DI +6. **Culture handling** - Many operations require explicit culture parameter +7. **Published vs Draft** - `IContent` is draft, `IPublishedContent` is published +8. **Constants** - Use constants instead of magic strings (property editor aliases, etc.) + +## Navigation Tips + +- **Finding a service**: Look in `/Services` for interfaces +- **Finding models**: Check `/Models` and subdirectories by domain +- **Finding notifications**: Browse `/Notifications` for available events +- **Finding configuration**: Check `/Configuration/Models` +- **Finding constants**: Search `Constants-*.cs` files +- **Understanding operation results**: Check `/Services/OperationStatus` + +## Further Resources + +- Implementation details are in **Umbraco.Infrastructure** +- Web functionality is in **Umbraco.Web.Common** +- API endpoints are in **Umbraco.Cms.Api.*** projects +- Official docs: https://docs.umbraco.com/ + +--- + +**Remember**: Umbraco.Core is about **defining what**, not **implementing how**. Keep implementations in Infrastructure! diff --git a/src/Umbraco.Infrastructure/CLAUDE.md b/src/Umbraco.Infrastructure/CLAUDE.md new file mode 100644 index 000000000000..84102b6df1cf --- /dev/null +++ b/src/Umbraco.Infrastructure/CLAUDE.md @@ -0,0 +1,777 @@ +# Umbraco CMS - Infrastructure + +Implementation layer for Umbraco CMS, providing concrete implementations of all Core interfaces. Handles database access (NPoco), caching, background jobs, migrations, search indexing (Examine), email (MailKit), and logging (Serilog). + +**Project**: `Umbraco.Infrastructure` +**Type**: .NET Library +**Files**: 1,006 C# files implementing Core contracts + +--- + +## 1. Architecture + +### Target Framework +- **.NET 10.0** (`net10.0`) +- **C# 12** with nullable reference types enabled +- **Library** (no executable) + +### Application Type +**Infrastructure Layer** - Implements all interfaces defined in `Umbraco.Core`. This is where contracts meet concrete implementations. + +### Key Technologies +- **Database Access**: NPoco (micro-ORM) with SQL Server & SQLite support +- **Caching**: In-memory + distributed cache via `IAppCache` +- **Background Jobs**: Recurring jobs via `IRecurringBackgroundJob` and `IDistributedBackgroundJob` +- **Search**: Examine (Lucene.NET wrapper) for full-text search +- **Email**: MailKit for SMTP email +- **Logging**: Serilog with structured logging +- **Migrations**: Custom migration framework for database schema + data +- **DI**: Microsoft.Extensions.DependencyInjection +- **Serialization**: System.Text.Json +- **Identity**: Microsoft.Extensions.Identity.Stores for user/member management +- **Authentication**: OpenIddict.Abstractions + +### Project Structure +``` +src/Umbraco.Infrastructure/ +├── Persistence/ # Database access (NPoco) +│ ├── Repositories/ # Repository implementations (47 repos) +│ │ └── Implement/ # Concrete repository classes +│ ├── Dtos/ # Database DTOs (80+ files) +│ ├── Mappers/ # Entity ↔ DTO mappers (43 mappers) +│ ├── Factories/ # Entity factories (28 factories) +│ ├── Querying/ # Query builders and translators +│ ├── SqlSyntax/ # SQL dialect handlers (SQL Server, SQLite) +│ ├── DatabaseModelDefinitions/ # Table/column definitions +│ └── UmbracoDatabase.cs # Main database wrapper (NPoco) +│ +├── Services/ # Service implementations +│ └── Implement/ # Concrete service classes (16 services) +│ ├── ContentService.cs # Content CRUD operations +│ ├── MediaService.cs # Media operations +│ ├── UserService.cs # User management +│ └── [13 more services...] +│ +├── Scoping/ # Unit of Work implementation +│ ├── ScopeProvider.cs # Transaction/scope management +│ ├── Scope.cs # Unit of work implementation +│ └── AmbientScopeContextStack.cs # Async-safe scope context +│ +├── Migrations/ # Database migration system +│ ├── Install/ # Initial database schema +│ ├── Upgrade/ # Version upgrade migrations (21 versions) +│ ├── PostMigrations/ # Post-upgrade data fixes +│ ├── MigrationPlan.cs # Migration orchestration +│ └── MigrationPlanExecutor.cs # Migration execution +│ +├── BackgroundJobs/ # Background job infrastructure +│ ├── Jobs/ # Concrete job implementations +│ │ ├── ReportSiteJob.cs # Telemetry reporting +│ │ ├── TempFileCleanupJob.cs # Cleanup temp files +│ │ └── ServerRegistration/ # Multi-server coordination +│ └── RecurringBackgroundJobHostedService.cs # Job scheduler +│ +├── Examine/ # Search indexing (Lucene) +│ ├── ContentValueSetBuilder.cs # Index document content +│ ├── MediaValueSetBuilder.cs # Index media +│ ├── MemberValueSetBuilder.cs # Index members +│ ├── DeliveryApiContentIndexPopulator.cs # Delivery API indexing +│ └── Deferred/ # Deferred index updates +│ +├── Security/ # Identity & authentication +│ ├── BackOfficeUserStore.cs # User store (Identity) +│ ├── MemberUserStore.cs # Member store (Identity) +│ ├── BackOfficeIdentity*.cs # Identity configuration +│ └── Passwords/ # Password hashing +│ +├── PropertyEditors/ # Property editor implementations (75 files) +│ ├── ValueConverters/ # Convert stored → typed values +│ ├── Validators/ # Property validation +│ ├── Configuration/ # Editor configuration +│ └── DeliveryApi/ # Delivery API converters +│ +├── Cache/ # Cache implementation & invalidation +│ ├── DatabaseServerMessengerNotificationHandler.cs # Multi-server cache sync +│ └── PropertyEditors/ # Property editor caching +│ +├── Serialization/ # JSON serialization (20 files) +│ ├── SystemTextJsonSerializer.cs # Main serializer +│ └── Converters/ # Custom JSON converters +│ +├── Mail/ # Email (MailKit) +│ ├── EmailSender.cs # SMTP email sender +│ └── EmailMessageExtensions.cs +│ +├── Logging/ # Serilog setup +│ ├── Serilog/ # Serilog enrichers +│ └── MessageTemplates.cs # Structured logging templates +│ +├── Mapping/ # Object mapping (IUmbracoMapper) +│ └── UmbracoMapper.cs # AutoMapper-like functionality +│ +├── Notifications/ # Notification handlers +├── Packaging/ # Package import/export +├── ModelsBuilder/ # Strongly-typed models generation +├── Routing/ # URL routing implementation +├── Runtime/ # Application lifecycle +├── Search/ # Search services +├── Sync/ # Multi-server synchronization +├── Telemetry/ # Analytics/telemetry +└── Templates/ # Template parsing +``` + +### Dependencies +- **Umbraco.Core** - All interface contracts (only dependency) +- **NPoco** - Micro-ORM for database access +- **Examine.Core** - Search indexing abstraction +- **MailKit** - Email sending +- **HtmlAgilityPack** - HTML parsing +- **Serilog** - Structured logging +- **ncrontab** - Cron expression parsing for background jobs +- **OpenIddict.Abstractions** - OAuth/OIDC abstractions +- **Microsoft.Extensions.Identity.Stores** - Identity storage + +### Design Patterns +1. **Repository Pattern** - All database access via repositories + - Example: `UserRepository`, `ContentRepository`, `MediaRepository` + - Implements interfaces from Umbraco.Core + +2. **Unit of Work** - Scoping pattern for transactions + - `IScopeProvider` / `Scope` - transaction management + - Must call `scope.Complete()` to commit + +3. **Factory Pattern** - Entity factories convert DTOs → domain models + - Example: `ContentFactory`, `MediaFactory`, `UserFactory` + - Located in `Persistence/Factories/` + +4. **Mapper Pattern** - Bidirectional DTO ↔ Entity mapping + - Example: `ContentMapper`, `MediaMapper`, `UserMapper` + - Located in `Persistence/Mappers/` + +5. **Strategy Pattern** - SQL syntax providers for different databases + - `SqlServerSyntaxProvider`, `SqliteSyntaxProvider` + - Abstracted via `ISqlSyntaxProvider` + +6. **Migration Pattern** - Version-based database migrations + - `MigrationPlan` defines migration graph + - `MigrationPlanExecutor` runs migrations in order + +7. **Builder Pattern** - `ValueSetBuilder` for search indexing + +--- + +## 2. Commands + +### Build & Test +```bash +# Build +dotnet build src/Umbraco.Infrastructure + +# Test (tests in ../../tests/) +dotnet test --filter "FullyQualifiedName~Infrastructure" + +# Pack +dotnet pack src/Umbraco.Infrastructure -c Release +``` + +### Code Quality +```bash +# Format code +dotnet format src/Umbraco.Infrastructure + +# Build with all warnings (note: many suppressed, see .csproj:31) +dotnet build src/Umbraco.Infrastructure /p:TreatWarningsAsErrors=true +``` + +### Database Migrations (Developer Context) +This project contains the migration framework but **migrations are NOT run via EF Core**. Migrations run at application startup via `MigrationPlanExecutor`. + +To create a new migration: +1. Create class inheriting `MigrationBase` in `Migrations/Upgrade/` +2. Add to `UmbracoPlan` migration plan +3. Restart application - migration runs automatically + +### Package Management +```bash +# Check for vulnerable packages +dotnet list src/Umbraco.Infrastructure package --vulnerable + +# Check outdated packages (versions in Directory.Packages.props) +dotnet list src/Umbraco.Infrastructure package --outdated +``` + +### Environment Setup +1. **Prerequisites**: .NET 10 SDK, SQL Server or SQLite +2. **IDE**: Visual Studio 2022, Rider, or VS Code +3. **Database**: Automatically created on first run (see Install/DatabaseSchemaCreator.cs) + +--- + +## 3. Style Guide + +### Project-Specific Patterns + +**Repository Naming** (from UserRepository.cs:30): +```csharp +internal sealed class UserRepository : EntityRepositoryBase, IUserRepository +``` +- Pattern: `{Entity}Repository : EntityRepositoryBase, I{Entity}Repository` +- Always `internal sealed` (not exposed outside assembly) +- Inherit from `EntityRepositoryBase` for common CRUD + +**Factory Naming** (consistent across Factories/ directory): +```csharp +internal class ContentFactory : IEntityFactory +``` +- Pattern: `{Entity}Factory : IEntityFactory` +- Converts DTO → Domain entity +- Always `internal` (implementation detail) + +**Service Naming** (from Services/Implement/): +```csharp +internal sealed class ContentService : RepositoryService, IContentService +``` +- Pattern: `{Domain}Service : RepositoryService, I{Domain}Service` +- Inherits `RepositoryService` for scope/repository access +- Always `internal sealed` + +### Key Code Patterns + +**Scope Usage** (required for all database operations): +```csharp +using (ICoreScope scope = ScopeProvider.CreateCoreScope()) +{ + // Database operations here + scope.Complete(); // MUST call to commit +} +``` + +**NPoco Query Building** (from repositories): +```csharp +Sql sql = Sql() + .Select() + .From() + .Where(x => x.NodeId == id); +``` + +--- + +## 4. Test Bench + +### Test Location +- **Unit Tests**: `tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/` +- **Integration Tests**: `tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/` +- **Benchmarks**: `tests/Umbraco.Tests.Benchmarks/` + +### Running Tests +```bash +# All Infrastructure tests +dotnet test --filter "FullyQualifiedName~Infrastructure" + +# Specific area (e.g., Persistence) +dotnet test --filter "FullyQualifiedName~Infrastructure.Persistence" + +# Integration tests only +dotnet test --filter "Category=Integration&FullyQualifiedName~Infrastructure" +``` + +### Testing Focus +1. **Repository tests** - CRUD operations, query translation +2. **Scope tests** - Transaction behavior, nested scopes +3. **Migration tests** - Schema creation, upgrade paths +4. **Service tests** - Business logic, notification firing +5. **Mapper tests** - DTO ↔ Entity conversion accuracy + +### InternalsVisibleTo +Tests have access to internal types (see .csproj:73-93): +- `Umbraco.Tests`, `Umbraco.Tests.UnitTests`, `Umbraco.Tests.Integration`, `Umbraco.Tests.Common`, `Umbraco.Tests.Benchmarks` +- `DynamicProxyGenAssembly2` (for Moq) + +--- + +## 5. Error Handling + +### Attempt Pattern (from Services) +Services return `Attempt` from Core: +```csharp +Attempt result = + await _contentService.CreateAsync(model, userKey); + +if (!result.Success) +{ + // result.Status contains typed error (e.g., ContentEditingOperationStatus.NotFound) +} +``` + +### Database Error Handling +**NPoco Exception Wrapping**: +- `NPocoSqlException` wraps database errors +- Check `InnerException` for underlying DB error +- SQL syntax errors logged via `ILogger` + +### Scope Error Handling +**Critical**: If scope not completed, transaction rolls back: +```csharp +using (ICoreScope scope = ScopeProvider.CreateCoreScope()) +{ + try + { + // Operations + scope.Complete(); // MUST be called + } + catch + { + // Transaction automatically rolled back if not completed + throw; + } +} +``` + +### Critical Logging Points +1. **Migration failures** - Logged in `MigrationPlanExecutor.cs` +2. **Database connection errors** - Logged in `UmbracoDatabaseFactory.cs` +3. **Repository exceptions** - Logged in `EntityRepositoryBase` +4. **Background job failures** - Logged in `RecurringBackgroundJobHostedService.cs` + +--- + +## 6. Clean Code + +### Key Design Decisions + +**Why NPoco instead of EF Core?** +- Performance: NPoco is faster for Umbraco's read-heavy workload +- Control: Fine-grained control over SQL generation +- Flexibility: Easier to optimize complex queries +- History: Legacy decision, but still valid (EF Core support added in parallel) + +**Why Custom Migration Framework?** +- Pre-dates EF Core Migrations +- Supports data migrations + schema migrations +- Graph-based dependencies (not linear like EF Core) +- Can target multiple database providers with same migration +- Located in `Migrations/` directory + +**Why EntityRepositoryBase?** +- DRY: Common CRUD operations (Get, GetMany, Save, Delete) +- Consistency: All repos follow same patterns +- Caching: Integrated cache invalidation +- Scoping: Automatic transaction management + +**Why Separate DTOs?** +- Database schema != domain model +- DTOs are flat, entities have relationships +- Allows independent evolution of schema vs domain +- Located in `Persistence/Dtos/` + +### Architectural Decisions + +**Repository Layer** (Persistence/Repositories/Implement/): +- 47 repository implementations +- All inherit from `EntityRepositoryBase` +- Repositories do NOT fire notifications (services do) + +**Service Layer** (Services/Implement/): +- 16 service implementations +- Services fire notifications before/after operations +- Services manage scopes (not repositories) +- Example: `ContentService`, `MediaService`, `UserService` + +### Code Smells to Watch For + +1. **Forgetting `scope.Complete()`** - Transaction silently rolls back +2. **Nested scopes without awareness** - Only innermost scope controls commit +3. **Lazy loading outside scope** - NPoco relationships must load within scope +4. **Large migrations** - Split into multiple steps if > 1000 lines +5. **Repository logic in services** - Keep repos thin, logic in services + +--- + +## 7. Security + +### Input Validation +**At Service Layer**: +- Services validate before calling repositories +- FluentValidation NOT used (manual validation) +- Example: `ContentService` validates content type exists before creating content + +### Data Access Security +**SQL Injection Prevention**: +- NPoco uses parameterized queries automatically +- Never concatenate SQL strings +- All queries via `Sql` builder: + ```csharp + Sql().Select().Where(x => x.NodeId == id) // Safe + Database.Query($"SELECT * FROM Content WHERE id = {id}") // NEVER do this + ``` + +### Authentication & Authorization +**Identity Stores** (Security/): +- `BackOfficeUserStore.cs` - User store for ASP.NET Core Identity +- `MemberUserStore.cs` - Member store for ASP.NET Core Identity +- Password hashing via `IPasswordHasher` (PBKDF2) + +**Password Security** (Security/Passwords/): +- PBKDF2 with 10,000 iterations (configurable) +- Salted hashes stored in database +- Legacy hash formats supported for migration + +### Secrets Management +**No secrets in this library** - Configuration from parent application (Umbraco.Web.UI): +- Connection strings from `appsettings.json` +- SMTP credentials from `appsettings.json` +- Email sender uses `IOptions` + +### Dependency Security +```bash +# Check vulnerable dependencies +dotnet list src/Umbraco.Infrastructure package --vulnerable +``` + +### Security Anti-Patterns to Avoid +1. **Raw SQL queries** - Always use NPoco `Sql` builder +2. **Storing plain text passwords** - Use Identity's password hasher +3. **Exposing internal types** - Keep repos/services `internal` +4. **Logging sensitive data** - Never log passwords, connection strings, API keys + +--- + +## 8. Teamwork and Workflow + +**⚠️ SKIPPED** - This is a sub-project. See root `/CLAUDE.md` for repository-wide teamwork protocols. + +--- + +## 9. Edge Cases + +### Scope Edge Cases + +**Nested Scopes** - Only innermost scope commits: +```csharp +using (var outer = ScopeProvider.CreateCoreScope()) +{ + using (var inner = ScopeProvider.CreateCoreScope()) + { + // Work + inner.Complete(); // This does nothing! + } + outer.Complete(); // This commits +} +``` + +**Async Scopes** - Scopes are NOT thread-safe: +- Don't pass scopes across threads +- Don't use scopes in Parallel.ForEach +- Create new scope on each async operation + +**SQLite Lock Contention**: +- SQLite has database-level locking +- Multiple concurrent writes = lock errors +- Use `[SuppressMessage]` for known SQLite lock issues +- See `UserRepository.cs:39` - `_sqliteValidateSessionLock` + +### Migration Edge Cases + +**Migration Rollback** - NOT SUPPORTED: +- Migrations are one-way only +- Test migrations thoroughly before release +- Use database backups for rollback + +**Migration Dependencies** - Graph-based: +- Migrations can have multiple dependencies +- Dependencies resolved via `MigrationPlan` +- Circular dependencies throw `InvalidOperationException` + +**Data Migrations** - Can be slow: +- Migrations run at startup (blocking) +- Large data migrations (> 100k rows) should be chunked +- Use `AsyncMigrationBase` for long-running operations + +### Repository Edge Cases + +**Cache Invalidation**: +- Repository CRUD operations invalidate cache automatically +- Bulk operations may not invalidate correctly +- Repositories fire cache refreshers via `DistributedCache` + +**NPoco Lazy Loading**: +- Relationships must be loaded within scope +- Accessing lazy-loaded properties outside scope throws +- Use `.FetchOneToMany()` to eager load + +### Background Job Edge Cases + +**Multi-Server Coordination**: +- Background jobs use server registration to coordinate +- Only "main" server runs jobs (determined by DB lock) +- If main server dies, another takes over within 5 minutes + +--- + +## 10. Agentic Workflow + +### When to Add a New Repository + +**Decision Points**: +1. Does the entity have a Core interface in `Umbraco.Core/Persistence/Repositories`? +2. Is this entity persisted to the database? +3. Does it require custom queries beyond basic CRUD? + +**Workflow**: +1. **Create DTO** in `Persistence/Dtos/` (matches database table) +2. **Create Mapper** in `Persistence/Mappers/` (DTO ↔ Entity) +3. **Create Factory** in `Persistence/Factories/` (DTO → Entity) +4. **Create Repository** in `Persistence/Repositories/Implement/` + - Inherit from `EntityRepositoryBase` + - Implement interface from Core +5. **Register in Composer** (DependencyInjection/) +6. **Write Tests** (unit + integration) + +### When to Add a New Service + +**Decision Points**: +1. Does the service have a Core interface in `Umbraco.Core/Services`? +2. Does it coordinate multiple repositories? +3. Does it need to fire notifications? + +**Workflow**: +1. **Implement Interface** from Core in `Services/Implement/` + - Inherit from `RepositoryService` + - Inject repositories via constructor +2. **Add Notification Firing**: + - Fire `*SavingNotification` before operation (cancellable) + - Fire `*SavedNotification` after operation +3. **Manage Scopes** - Services create scopes, not repositories +4. **Register in Composer** +5. **Write Tests** + +### When to Add a Migration + +**Decision Points**: +1. Is this a schema change (tables, columns, indexes)? +2. Is this a data migration (update existing data)? +3. Which version does this target? + +**Workflow**: +1. **Create Migration Class** in `Migrations/Upgrade/V{Version}/` + - Inherit from `MigrationBase` (schema) or `AsyncMigrationBase` (data) + - Implement `Migrate()` method +2. **Add to UmbracoPlan** in `Migrations/Upgrade/UmbracoPlan.cs` + - Specify dependencies (runs after which migrations?) +3. **Test Migration**: + - Integration test with database + - Test upgrade from previous version +4. **Document Breaking Changes** (if any) + +### Quality Gates Before PR +1. All tests pass +2. Code formatted (`dotnet format`) +3. No new warnings (check suppressed warnings list in .csproj:31) +4. Database migrations tested (upgrade from previous version) +5. Scope usage correct (all scopes completed) + +### Common Pitfalls +1. **Forgetting `scope.Complete()`** - Transaction rolls back silently +2. **Repository logic in services** - Keep repos focused on data access +3. **Missing cache invalidation** - Repositories auto-invalidate, but custom queries may not +4. **Missing notifications** - Services must fire notifications +5. **Eager loading outside scope** - NPoco relationships must load within scope +6. **Large migrations** - Chunk data migrations for performance + +--- + +## 11. Project-Specific Notes + +### Key Design Decisions + +**Why 1,006 files for "just" implementation?** +- 47 repositories × ~3 files each (repo, mapper, factory) = ~141 files +- 75 property editors × ~2 files each = ~150 files +- 80 DTOs for database tables = 80 files +- 21 versions × ~5 migrations each = ~105 files +- Remaining: services, background jobs, search, email, logging, etc. + +**Why NPoco + Custom Migrations?** +- Historical: Predates EF Core +- Performance: Faster than EF Core for Umbraco's workload +- Control: Fine-grained SQL control +- **Note**: EF Core support added in parallel (`Umbraco.Cms.Persistence.EFCore` project) + +**Why Separate Factories and Mappers?** +- **Factories**: DTO → Entity (one direction, for reading from DB) +- **Mappers**: DTO ↔ Entity (bidirectional, includes column mapping metadata) +- Factories use Mappers under the hood + +### External Integrations + +**Email (MailKit)**: +- SMTP email via `MailKit` library (version in `Directory.Packages.props`) +- Configured via `IOptions` +- Supports TLS, SSL, authentication + +**Search (Examine)**: +- Lucene.NET wrapper +- Indexes content, media, members +- `ValueSetBuilder` classes convert entities → search documents +- Located in `Examine/` directory + +**Logging (Serilog)**: +- Structured logging throughout +- Enrichers: Process ID, Thread ID +- Sinks: File, Async +- Configuration in `appsettings.json` of parent app + +**Background Jobs (Recurring)**: +- Cron-based scheduling via `ncrontab` +- `IRecurringBackgroundJob` interface +- Jobs: Telemetry, temp file cleanup, server registration +- Located in `BackgroundJobs/Jobs/` + +### Known Limitations + +1. **NPoco Lazy Loading** - Must load relationships within scope +2. **Migration Rollback** - One-way only, no rollback support +3. **SQLite Locking** - Database-level locks cause contention +4. **Single Database** - No multi-database support (e.g., read replicas) +5. **Background Jobs** - Single-server only (distributed jobs require additional setup) + +### Performance Considerations + +**Caching**: +- Repository results cached automatically +- Cache invalidation via `DistributedCache` +- Multi-server cache sync via database messenger + +**Database Connection Pooling**: +- ADO.NET connection pooling enabled by default +- Configured in connection string + +**N+1 Query Problem**: +- NPoco supports eager loading via `.FetchOneToMany()` +- Always profile queries in development + +**Background Jobs**: +- Run on background threads (don't block web requests) +- Use `IRecurringBackgroundJob` for scheduled tasks +- Use `IDistributedBackgroundJob` for multi-server coordination + +### Technical Debt (from TODO comments in .csproj and code) + +1. **Warnings Suppressed** (Umbraco.Infrastructure.csproj:10-30): + ``` + TODO: Fix and remove overrides: + - CS0618: handle member obsolete appropriately + - CA1416: validate platform compatibility + - SA1117: params all on same line + - SA1401: make fields private + - SA1134: own line attributes + - CA2017: match parameters number + - CS0108: hidden inherited member + - SYSLIB0051: formatter-based serialization + - SA1649: filename match type name + - CS1998: remove async or make method synchronous + - CS0169: unused field + - CS0114: hidden inherited member + - IDE0060: remove unused parameter + - SA1130: use lambda syntax + - IDE1006: naming violation + - CS1066: default value + - CS0612: obsolete + - CS1574: resolve cref + ``` + +2. **CacheInstructionService.cs** - TODO comments (multiple locations) + +3. **MemberUserStore.cs** - TODO: Handle external logins + +4. **BackOfficeUserStore.cs** - TODO: Optimize user queries + +5. **UserRepository.cs** - TODO: SQLite session validation lock (line 39) + +6. **Repository Base Classes** - Some repos have large inheritance chains (tech debt) + +### TRACE_SCOPES Feature +**Debug-only scope tracing** (Umbraco.Infrastructure.csproj:34-36): +```xml + + $(DefineConstants);TRACE_SCOPES + +``` +- Enables detailed scope logging in Debug builds +- Helps debug nested scope issues +- Performance impact - only use in development + +--- + +## Quick Reference + +### Essential Commands +```bash +# Build +dotnet build src/Umbraco.Infrastructure + +# Test Infrastructure +dotnet test --filter "FullyQualifiedName~Infrastructure" + +# Format code +dotnet format src/Umbraco.Infrastructure + +# Pack for NuGet +dotnet pack src/Umbraco.Infrastructure -c Release +``` + +### Key Dependencies +- **Umbraco.Core** - Interface contracts (only project dependency) +- **NPoco** - Database access +- **Examine.Core** - Search indexing +- **MailKit** - Email sending +- **Serilog** - Logging + +### Important Files +- **Project file**: `src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj` +- **Scope Provider**: `src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs` +- **Database Factory**: `src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs` +- **Migration Executor**: `src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs` +- **Content Service**: `src/Umbraco.Infrastructure/Services/Implement/ContentService.cs` +- **User Repository**: `src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs` + +### Critical Patterns +```csharp +// 1. Always use scopes for database operations +using (ICoreScope scope = ScopeProvider.CreateCoreScope()) +{ + // Work + scope.Complete(); // MUST call to commit +} + +// 2. NPoco query building +Sql sql = Sql() + .Select() + .From() + .Where(x => x.NodeId == id); + +// 3. Repository pattern +IContent? content = _contentRepository.Get(id); + +// 4. Service pattern with notifications +var saving = new ContentSavingNotification(content, eventMessages); +if (_eventAggregator.PublishCancelable(saving)) + return Attempt.Fail(...); // Cancelled + +_contentRepository.Save(content); + +var saved = new ContentSavedNotification(content, eventMessages); +_eventAggregator.Publish(saved); +``` + +### Configuration +No appsettings in this library - all configuration from parent application (Umbraco.Web.UI): +- Connection strings +- Email settings +- Serilog configuration +- Background job schedules + +### Getting Help +- **Core Docs**: `../Umbraco.Core/CLAUDE.md` (interface contracts) +- **Root Docs**: `/CLAUDE.md` (repository overview) +- **Official Docs**: https://docs.umbraco.com/umbraco-cms/reference/ diff --git a/src/Umbraco.Web.UI.Client/CLAUDE.md b/src/Umbraco.Web.UI.Client/CLAUDE.md new file mode 100644 index 000000000000..b7400e9d8442 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/CLAUDE.md @@ -0,0 +1,89 @@ +# Umbraco Backoffice - @umbraco-cms/backoffice + +Modern TypeScript/Lit-based web components library for the Umbraco CMS backoffice interface. This project provides extensible UI components, APIs, and utilities for building the Umbraco CMS administration interface. + +**Package**: `@umbraco-cms/backoffice` +**Version**: 17.1.0-rc +**License**: MIT +**Repository**: https://github.com/umbraco/Umbraco-CMS +**Live Preview**: https://backofficepreview.umbraco.com/ + +--- + +## Documentation Structure + +This project's documentation is organized into 9 focused guides: + +**Note**: This is a sub-project in the Umbraco CMS monorepo. For Git workflow, PR process, and CI/CD information, see the [repository root CLAUDE.md](../../CLAUDE.md). + +### Architecture & Design +- **[Architecture](./docs/architecture.md)** - Technology stack, design patterns, module organization + +### Development +- **[Commands](./docs/commands.md)** - Build, test, and development commands + +### Code Quality +- **[Style Guide](./docs/style-guide.md)** - Naming and formatting conventions +- **[Clean Code](./docs/clean-code.md)** - Best practices and SOLID principles +- **[Testing](./docs/testing.md)** - Unit, integration, and E2E testing strategies + +### Troubleshooting +- **[Error Handling](./docs/error-handling.md)** - Error patterns and debugging +- **[Edge Cases](./docs/edge-cases.md)** - Common pitfalls and gotchas + +### Security & AI +- **[Security](./docs/security.md)** - XSS prevention, authentication, input validation +- **[Agentic Workflow](./docs/agentic-workflow.md)** - Three-phase AI development process + +--- + +## Quick Start + +### Prerequisites + +- **Node.js**: >=22.17.1 +- **npm**: >=10.9.2 +- Modern browser (Chrome, Firefox, Safari) + +### Initial Setup + +```bash +# 1. Clone repository +git clone https://github.com/umbraco/Umbraco-CMS.git +cd Umbraco-CMS/src/Umbraco.Web.UI.Client + +# 2. Install dependencies +npm install + +# 3. Start development +npm run dev +``` + +### Most Common Commands + +See **[Commands](./docs/commands.md)** for all available commands. + +**Development**: `npm run dev` | **Testing**: `npm test` | **Build**: `npm run build` | **Lint**: `npm run lint:fix` + +--- + +## Quick Reference + +| Category | Details | +|----------|---------| +| **Apps** | `src/apps/` - Application entry points (app, installer, upgrader) | +| **Libraries** | `src/libs/` - Core APIs (element-api, context-api, controller-api) | +| **Packages** | `src/packages/` - Feature packages; `src/packages/core/` for utilities | +| **External** | `src/external/` - Dependency wrappers (lit, rxjs, luxon) | +| **Mocks** | `src/mocks/` - MSW handlers and mock data | +| **Config** | `package.json`, `vite.config.ts`, `.env` (create `.env.local`) | +| **Elements** | Custom elements use `umb-{feature}-{component}` pattern | + +### Getting Help + +**Documentation**: [UI API Docs](npm run generate:ui-api-docs) | [Storybook](npm run storybook) | [Official Docs](https://docs.umbraco.com/) +**Community**: [Issues](https://github.com/umbraco/Umbraco-CMS/issues) | [Discussions](https://github.com/umbraco/Umbraco-CMS/discussions) | [Forum](https://our.umbraco.com/) + +--- + +**This project follows a modular package architecture with strict TypeScript, Lit web components, and an extensible manifest system. Each package is independent but follows consistent patterns. For extension development, use the Context API for dependency injection, controllers for logic, and manifests for registration.** diff --git a/src/Umbraco.Web.UI.Client/docs/agentic-workflow.md b/src/Umbraco.Web.UI.Client/docs/agentic-workflow.md new file mode 100644 index 000000000000..a762678733ed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/agentic-workflow.md @@ -0,0 +1,457 @@ +# Agentic Workflow +[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md) + +--- + +### Phase 1: Analysis & Planning + +**Understand Requirements**: +1. Read user request carefully +2. Identify acceptance criteria +3. Ask clarifying questions if ambiguous +4. Determine scope (new feature, bug fix, refactor, etc.) + +**Research Codebase**: +1. Find similar implementations in codebase +2. Identify patterns and conventions used +3. Locate relevant packages and modules +4. Review existing tests for similar features +5. Check for existing utilities or helpers + +**Identify Technical Approach**: +1. Which packages need changes? + - New package or modify existing? + - Core infrastructure or feature package? +2. What patterns to use? + - Web Component, Controller, Repository, Context? + - Extension type? (dashboard, workspace, modal, etc.) +3. Dependencies needed? + - New npm packages? + - Internal package dependencies? +4. API changes needed? + - New OpenAPI endpoints? + - Changes to existing endpoints? + +**Break Down Implementation**: +1. **Models/Types** - Define TypeScript interfaces and types +2. **API Client** - Update OpenAPI client if backend changes +3. **Repository** - Data access layer +4. **Store/Context** - State management +5. **Controller** - Business logic +6. **Element** - UI component +7. **Manifest** - Extension registration +8. **Tests** - Unit and integration tests +9. **Documentation** - JSDoc and examples +10. **Storybook** (if applicable) - Component stories + +**Consider Architecture**: +- Does this follow project patterns? +- Are dependencies correct? (libs → packages → apps) +- Will this create circular dependencies? +- Is this extensible for future needs? +- Performance implications? + +**Document Plan**: +- Write brief implementation plan +- Identify potential issues or blockers +- Get approval from user if significant changes + +### Phase 2: Incremental Implementation + +**For New Feature (Component/Package)**: + +**Step 1: Define Types** +```typescript +// 1. Create model interface +export interface UmbMyModel { + id: string; + name: string; + description?: string; +} + +// 2. Create manifest type +export interface UmbMyManifest extends UmbManifestBase { + type: 'myType'; + // ... specific properties +} +``` + +**Verify**: TypeScript compiles, no errors + +**Step 2: Create Repository** +```typescript +// 3. Data access layer +export class UmbMyRepository { + async requestById(id: string) { + // Fetch from API + // Return { data } or { error } + } +} +``` + +**Verify**: Repository compiles, basic structure correct + +**Step 3: Create Store/Context** +```typescript +// 4. State management +export class UmbMyStore extends UmbStoreBase { + // Observable state +} + +export const UMB_MY_CONTEXT = new UmbContextToken('UmbMyContext'); + +export class UmbMyContext extends UmbControllerBase { + #repository = new UmbMyRepository(); + #store = new UmbMyStore(this); + + // Public API +} +``` + +**Verify**: Context compiles, can be consumed + +**Step 4: Create Element** +```typescript +// 5. UI component +@customElement('umb-my-element') +export class UmbMyElement extends UmbElementMixin(LitElement) { + #context?: UmbMyContext; + + constructor() { + super(); + this.consumeContext(UMB_MY_CONTEXT, (context) => { + this.#context = context; + }); + } + + render() { + return html`
My Element
`; + } +} +``` + +**Verify**: Element renders in browser, no console errors + +**Step 5: Wire Up Interactions** +```typescript +// 6. Connect user interactions to logic +@customElement('umb-my-element') +export class UmbMyElement extends UmbElementMixin(LitElement) { + async #handleClick() { + const { data, error } = await this.#context?.load(); + if (error) { + this._error = error; + return; + } + this._data = data; + } + + render() { + return html` + + ${this._data ? html`

${this._data.name}

` : ''} + `; + } +} +``` + +**Verify**: Interactions work, data flows correctly + +**Step 6: Add Extension Manifest** +```typescript +// 7. Register extension +export const manifest: UmbManifestMyType = { + type: 'myType', + alias: 'My.Extension', + name: 'My Extension', + element: () => import('./my-element.element.js'), +}; +``` + +**Verify**: Extension loads, manifest is valid + +**Step 7: Write Tests** +```typescript +// 8. Unit tests +describe('UmbMyElement', () => { + it('should render', async () => { + const element = await fixture(html``); + expect(element).to.exist; + }); + + it('should load data', async () => { + // Test data loading + }); +}); +``` + +**Verify**: Tests pass + +**Step 8: Add Error Handling** +```typescript +// 9. Handle errors gracefully +async #handleClick() { + try { + this._loading = true; + const { data, error } = await this.#context?.load(); + if (error) { + this._error = 'Failed to load data'; + return; + } + this._data = data; + } catch (error) { + this._error = 'Unexpected error occurred'; + console.error('Load failed:', error); + } finally { + this._loading = false; + } +} +``` + +**Verify**: Errors are handled, UI shows error state + +**After Each Step**: +- ✅ Code compiles (no TypeScript errors) +- ✅ Tests pass (existing and new) +- ✅ Follows patterns (consistent with codebase) +- ✅ No breaking changes (or documented) +- ✅ ESLint passes +- ✅ Commit working code + +### Phase 3: Review & Refinement + +**Run All Quality Checks**: + +```bash +# 1. Run all tests +npm test + +# 2. Run lint +npm run lint:errors + +# 3. Type check +npm run compile + +# 4. Build +npm run build + +# 5. Check circular dependencies +npm run check:circular + +# 6. Validate package exports +npm run package:validate +``` + +**Code Review Checklist**: +- [ ] Code follows style guide +- [ ] All tests pass +- [ ] New tests added for new code +- [ ] TypeScript types are correct +- [ ] No `any` types (or justified) +- [ ] JSDoc comments on public APIs +- [ ] Error handling in place +- [ ] Edge cases handled +- [ ] No console.log in production code +- [ ] Accessibility considered (for UI components) +- [ ] Performance acceptable +- [ ] No memory leaks (subscriptions cleaned up) +- [ ] Follows existing patterns +- [ ] No circular dependencies introduced + +**Documentation**: +- [ ] JSDoc on public APIs +- [ ] Update README if needed +- [ ] Add Storybook story (for components) +- [ ] Add example code (for APIs) +- [ ] Update CLAUDE.md (for significant changes) + +### Reasoning & Decision-Making + +**Architecture Decisions**: +- **Follow existing patterns** - Don't invent new patterns unless necessary +- **Use Context API** - For cross-component state and dependencies +- **Controllers for logic** - Separate business logic from UI +- **Repositories for data** - Abstract data access +- **Manifests for extensions** - Register features via extension system + +**Component Design Decisions**: +- **Small components** - Break complex UI into smaller components +- **Single responsibility** - Each component does one thing well +- **Composition** - Combine small components to build complex UI +- **Props down, events up** - Data flows down via properties, changes flow up via events + +**State Management Decisions**: +- **Local state** - `@state()` for component-specific state +- **Shared state** - Context and stores for cross-component state +- **Observables** - RxJS for reactive data streams +- **Immutability** - Don't mutate state, create new objects + +**API Design Decisions**: +- **Async by default** - All data operations are async +- **Error objects** - Return `{ data, error }` not throw exceptions +- **TypeScript types** - Strong typing for all APIs +- **Consistent naming** - Follow existing naming conventions + +### Think-Aloud Process + +**Example: "I need to add a bulk action for content"** + +**Analysis**: +> "I need to add a bulk action for content. Let me think through this: +> +> 1. What is a bulk action? It's an action that operates on multiple items. +> 2. Where do bulk actions appear? In collection views (document tree, list view). +> 3. What pattern is used? Let me check existing bulk actions... +> 4. Found: Block editor has bulk actions. Let me review that code. +> 5. Architecture: Bulk actions are registered via manifests (type: 'entityBulkAction') +> 6. Implementation needs: +> - Manifest to register action +> - Element to show confirmation dialog +> - Repository method to perform bulk operation +> - Permission check (can user do this?) +> 7. Which package? This goes in `packages/documents/documents/` since it's document-specific. +> 8. Dependencies: Need `@umbraco-cms/backoffice/entity-bulk-action`, modal, repository." + +**Planning**: +> "Implementation steps: +> 1. Create bulk action manifest in documents package +> 2. Create modal element for confirmation +> 3. Add repository method for bulk operation +> 4. Add permission check +> 5. Wire up action to modal +> 6. Test with multiple documents +> 7. Handle errors (partial success, permissions, etc.) +> 8. Add loading state +> 9. Show success/error notification +> 10. Write tests" + +**Implementation**: +> "Starting with step 1: Create manifest. +> Looking at existing bulk actions, I see the pattern uses `UmbEntityBulkActionBase`. +> Let me create the manifest following that pattern..." + +### Error Recovery + +**If Tests Fail**: +1. Read the error message carefully +2. Reproduce the failure locally +3. Debug with console.log or debugger +4. Understand root cause (don't just fix symptoms) +5. Fix the underlying issue +6. Verify fix doesn't break other tests +7. Add test for the bug (if it was a real bug) + +**If Architecture Feels Wrong**: +1. Pause and reconsider +2. Review similar implementations in codebase +3. Discuss with team (via PR comments) +4. Don't force a pattern that doesn't fit +5. Refactor if needed + +**If Introducing Breaking Changes**: +1. Discuss with user first +2. Document the breaking change +3. Provide migration path +4. Update CHANGELOG +5. Consider deprecation instead + +**If Stuck**: +1. Ask for clarification from user +2. Review documentation +3. Look at similar code in repository +4. Break problem into smaller pieces +5. Try a simpler approach first + +### Quality Gates Checklist + +Before marking work as complete: + +**Code Quality**: +- [ ] All new code has unit tests +- [ ] Integration tests for workflows (if applicable) +- [ ] All tests pass (including existing tests) +- [ ] Code follows style guide (ESLint passes) +- [ ] Prettier formatting correct +- [ ] No TypeScript errors +- [ ] JSDoc comments on public functions +- [ ] No `console.log` in production code +- [ ] No commented-out code + +**Security**: +- [ ] Input validation in place +- [ ] No XSS vulnerabilities +- [ ] No sensitive data logged +- [ ] User input sanitized +- [ ] Permissions checked (for sensitive operations) + +**Performance**: +- [ ] No obvious performance issues +- [ ] No memory leaks (subscriptions cleaned up) +- [ ] Efficient algorithms used +- [ ] Large lists use virtualization (if needed) + +**Architecture**: +- [ ] Follows existing patterns +- [ ] No circular dependencies +- [ ] Correct package dependencies +- [ ] Extension manifest correct (if applicable) + +**Documentation**: +- [ ] JSDoc on public APIs +- [ ] README updated (if needed) +- [ ] Examples added (if new API) +- [ ] Breaking changes documented + +**User Experience** (for UI changes): +- [ ] Accessible (keyboard navigation, screen readers) +- [ ] Responsive (works on different screen sizes) +- [ ] Loading states shown +- [ ] Errors handled gracefully +- [ ] Success feedback provided + +### Communication + +**After Each Phase**: +- Summarize what was implemented +- Highlight key decisions and why +- Call out any blockers or questions +- Show progress (code snippets, screenshots for UI) + +**When Making Decisions**: +- Explain reasoning +- Reference existing patterns +- Call out trade-offs +- Ask for confirmation on significant changes + +**When Blocked**: +- Clearly state the blocker +- Explain what you've tried +- Ask specific questions +- Suggest potential approaches + +**When Complete**: +- Summarize what was delivered +- Note any deviations from plan (and why) +- Highlight testing performed +- Mention any follow-up work needed + +### For Large Features + +**Multi-Session Features**: +1. Create feature branch (if spanning multiple PRs) +2. Break into multiple PRs if very large: + - PR 1: Infrastructure (types, repository, context) + - PR 2: UI components + - PR 3: Extensions and integration +3. Keep main stable - only merge completed, tested features +4. Consider feature flags for gradual rollout +5. Document progress and remaining work in PR description + +**Coordination**: +- Update PR description with progress +- Use GitHub task lists in PR description +- Comment on PR with status updates +- Request early feedback on architecture + +--- + diff --git a/src/Umbraco.Web.UI.Client/docs/architecture.md b/src/Umbraco.Web.UI.Client/docs/architecture.md new file mode 100644 index 000000000000..f4189913a6c6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/architecture.md @@ -0,0 +1,124 @@ +# Architecture +[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md) + +--- + + +### Technology Stack + +- **Node.js**: >=22.17.1 +- **npm**: >=10.9.2 +- **Language**: TypeScript 5.9.3 +- **Module System**: ESM (ES Modules) +- **Framework**: Lit (Web Components) +- **Build Tool**: Vite 7.1.11 +- **Test Framework**: @web/test-runner with Playwright +- **E2E Testing**: Playwright 1.55.1 +- **Code Quality**: ESLint 9.37.0, Prettier 3.6.2 +- **Mocking**: MSW (Mock Service Worker) 1.3.5 +- **Documentation**: Storybook 9.0.14, TypeDoc 0.28.13 + +### Application Type + +Single-page web application (SPA) packaged as an npm library. Provides: +- Extensible Web Components for CMS backoffice UI +- API libraries for extension development +- TypeScript type definitions +- Package manifest system for extensions + +### Architecture Pattern + +**Modular Package Architecture** with clear separation: + +``` +src/ +├── apps/ # Application entry points +│ ├── app/ # Main backoffice application +│ ├── backoffice/ # Backoffice shell +│ ├── installer/ # CMS installer interface +│ ├── preview/ # Content preview +│ └── upgrader/ # CMS upgrader interface +│ +├── libs/ # Core API libraries (infrastructure) +│ ├── class-api/ # Base class utilities +│ ├── context-api/ # Context API for dependency injection +│ ├── context-proxy/ # Context proxying utilities +│ ├── controller-api/ # Controller lifecycle management +│ ├── element-api/ # Element base classes & mixins +│ ├── extension-api/ # Extension registration & loading +│ ├── localization-api/ # Internationalization +│ └── observable-api/ # Reactive state management +│ +├── packages/ # Feature packages (50+ packages) +│ ├── core/ # Core utilities (auth, http, router, etc.) +│ ├── content/ # Content management +│ ├── documents/ # Document types & editing +│ ├── media/ # Media management +│ ├── members/ # Member management +│ ├── user/ # User management +│ ├── templating/ # Templates, scripts, stylesheets +│ ├── block/ # Block editor components +│ └── ... # 30+ more specialized packages +│ +├── external/ # External dependency wrappers +│ ├── lit/ # Lit framework wrapper +│ ├── rxjs/ # RxJS wrapper +│ ├── luxon/ # Date/time library wrapper +│ ├── monaco-editor/# Code editor wrapper +│ └── ... # Other wrapped dependencies +│ +├── mocks/ # MSW mock handlers & test data +│ ├── data/ # Mock database +│ └── handlers/ # API request handlers +│ +└── assets/ # Static assets (fonts, images, localization) +``` + +### Design Patterns + +1. **Web Components** - Custom elements with Shadow DOM encapsulation +2. **Context API** - Dependency injection via context providers/consumers (similar to React Context) +3. **Controller Pattern** - Lifecycle-aware controllers for managing component behavior +4. **Extension System** - Manifest-based plugin architecture +5. **Observable Pattern** - Reactive state management with RxJS observables +6. **Repository Pattern** - Data access abstraction via repository classes +7. **Mixin Pattern** - Composable behaviors via TypeScript mixins (`UmbElementMixin`, etc.) +8. **Builder Pattern** - For complex object construction +9. **Registry Pattern** - Extension registry for dynamic feature loading +10. **Observer Pattern** - Event-driven communication between components + +### Key Technologies + +**Core Framework**: +- Lit 3.x - Web Components framework with reactive templates +- TypeScript 5.9 - Type-safe development with strict mode +- Vite - Fast build tool and dev server + +**UI Components**: +- @umbraco-ui/uui - Umbraco UI component library +- Shadow DOM - Component style encapsulation +- Custom Elements API - Native web components + +**State & Data**: +- RxJS - Reactive programming with observables +- Context API - State management & dependency injection +- MSW - API mocking for development & testing + +**Testing**: +- @web/test-runner - Fast test runner for web components +- Playwright - E2E browser testing +- @open-wc/testing - Testing utilities for web components + +**Code Quality**: +- ESLint with TypeScript plugin - Linting with strict rules +- Prettier - Code formatting +- TypeDoc - API documentation generation +- Web Component Analyzer - Custom element documentation + +**Other**: +- Luxon - Date/time manipulation +- Monaco Editor - Code editing +- DOMPurify - HTML sanitization +- Marked - Markdown parsing +- SignalR - Real-time communication + diff --git a/src/Umbraco.Web.UI.Client/docs/clean-code.md b/src/Umbraco.Web.UI.Client/docs/clean-code.md new file mode 100644 index 000000000000..1f7a679c0c6f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/clean-code.md @@ -0,0 +1,397 @@ +# Clean Code +[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md) + +--- + + +### Function Design + +**Function Length**: +- Target: ≤30 lines per function +- Max: 50 lines (enforce via code review) +- Extract complex logic to separate functions +- Use early returns to reduce nesting + +**Single Responsibility**: +```typescript +// Good - Single responsibility +private _validateName(name: string): boolean { + return name.length > 0 && name.length <= 100; +} + +private _sanitizeName(name: string): string { + return name.trim().replace(/[<>]/g, ''); +} + +// Bad - Multiple responsibilities +private _processName(name: string): { valid: boolean; sanitized: string } { + // Doing too much in one function +} +``` + +**Descriptive Names**: +- Use verb names for functions: `loadData`, `validateInput`, `handleClick` +- Boolean functions: `is`, `has`, `can`, `should` prefix +- Event handlers: `handle`, `on` prefix: `handleSubmit`, `onClick` + +**Parameters**: +- Limit to 3-4 parameters +- Use options object for more parameters: + +```typescript +// Good - Options object +interface LoadOptions { + id: string; + includeChildren?: boolean; + depth?: number; + culture?: string; +} + +private _load(options: LoadOptions) { } + +// Bad - Too many parameters +private _load(id: string, includeChildren: boolean, depth: number, culture: string) { } +``` + +**Early Returns**: + +```typescript +// Good - Early returns reduce nesting +private _validate(): boolean { + if (!this._data) return false; + if (!this._data.name) return false; + if (this._data.name.length === 0) return false; + return true; +} + +// Bad - Nested conditions +private _validate(): boolean { + if (this._data) { + if (this._data.name) { + if (this._data.name.length > 0) { + return true; + } + } + } + return false; +} +``` + +### Class Design + +**Single Responsibility**: +- Each class should have one reason to change +- Controllers handle one aspect of behavior +- Repositories handle one entity type +- Components handle one UI concern + +**Small Classes**: +- Target: <300 lines per class +- Extract complex logic to separate controllers/utilities +- Use composition over inheritance + +**Encapsulation**: + +```typescript +export class UmbMyElement extends LitElement { + // Public API - reactive properties + @property({ type: String }) + value = ''; + + // Private state + @state() + private _loading = false; + + // Private fields (not reactive) + #controller = new UmbMyController(this); + + // Private methods + private _loadData() { } +} +``` + +### SOLID Principles (Adapted for TypeScript/Lit) + +**S - Single Responsibility**: +- One component = one UI responsibility +- One controller = one behavior responsibility +- One repository = one entity type + +**O - Open/Closed**: +- Extend via composition and mixins +- Extension API for plugins +- Avoid modifying existing components, create new ones + +**L - Liskov Substitution**: +- Subclasses should honor base class contracts +- Use interfaces for polymorphism + +**I - Interface Segregation**: +- Small, focused interfaces +- Use TypeScript `interface` for contracts + +**D - Dependency Inversion**: +- Depend on abstractions (interfaces) not concrete classes +- Use Context API for dependency injection +- Controllers receive dependencies via constructor + +### Dependency Injection + +**Context API** (Preferred): + +```typescript +export class UmbMyElement extends UmbElementMixin(LitElement) { + #authContext?: UmbAuthContext; + + constructor() { + super(); + + // Consume context (dependency injection) + this.consumeContext(UMB_AUTH_CONTEXT, (context) => { + this.#authContext = context; + }); + } +} +``` + +**Controller Pattern**: + +```typescript +export class UmbMyController extends UmbControllerBase { + #repository: UmbContentRepository; + + constructor(host: UmbControllerHost, repository: UmbContentRepository) { + super(host); + this.#repository = repository; + } +} + +// Usage +#controller = new UmbMyController(this, new UmbContentRepository()); +``` + +### Avoid Code Smells + +**Magic Numbers/Strings**: + +```typescript +// Bad +if (status === 200) { } +if (type === 'document') { } + +// Good +const HTTP_OK = 200; +const CONTENT_TYPE_DOCUMENT = 'document'; + +if (status === HTTP_OK) { } +if (type === CONTENT_TYPE_DOCUMENT) { } + +// Or use enums +enum ContentType { + Document = 'document', + Media = 'media', +} +``` + +**Long Parameter Lists**: + +```typescript +// Bad +function create(name: string, type: string, parent: string, culture: string, template: string) { } + +// Good +interface CreateOptions { + name: string; + type: string; + parent?: string; + culture?: string; + template?: string; +} + +function create(options: CreateOptions) { } +``` + +**Duplicate Code**: +- Extract to shared functions +- Use composition and mixins +- Create utility modules + +**Deeply Nested Code**: +- Use early returns +- Extract to separate functions +- Use guard clauses + +**Callback Hell** (N/A - use async/await) + +### Modern Patterns + +**Async/Await**: + +```typescript +// Good - Clean async code +async loadContent() { + try { + this._loading = true; + const { data, error } = await this.repository.requestById(this.id); + if (error) { + this._error = error; + return; + } + this._content = data; + } finally { + this._loading = false; + } +} +``` + +**Optional Chaining**: + +```typescript +// Good - Safe property access +const name = this._content?.variants?.[0]?.name; + +// Bad - Manual null checks +const name = this._content && this._content.variants && + this._content.variants[0] && this._content.variants[0].name; +``` + +**Destructuring**: + +```typescript +// Good - Destructure for clarity +const { name, description, icon } = this._content; + +// Good - With defaults +const { name = 'Untitled', description = '' } = this._content; +``` + +**Immutability**: + +```typescript +// Good - Spread operator for immutability +this._items = [...this._items, newItem]; +this._config = { ...this._config, newProp: value }; + +// Bad - Mutation +this._items.push(newItem); +this._config.newProp = value; +``` + +**Pure Functions**: + +```typescript +// Good - Pure function (no side effects) +function calculateTotal(items: Item[]): number { + return items.reduce((sum, item) => sum + item.price, 0); +} + +// Bad - Impure (modifies input) +function calculateTotal(items: Item[]): number { + items.sort((a, b) => a.price - b.price); // Mutation! + return items.reduce((sum, item) => sum + item.price, 0); +} +``` + +### TypeScript-Specific Patterns + +**Type Guards**: + +```typescript +function isContentModel(value: unknown): value is UmbContentModel { + return typeof value === 'object' && value !== null && 'id' in value; +} + +// Usage +if (isContentModel(data)) { + // TypeScript knows data is UmbContentModel + console.log(data.id); +} +``` + +**Discriminated Unions**: + +```typescript +type Result = + | { success: true; data: T } + | { success: false; error: Error }; + +function handleResult(result: Result) { + if (result.success) { + console.log(result.data); // TypeScript knows this exists + } else { + console.error(result.error); // TypeScript knows this exists + } +} +``` + +**Utility Types**: + +```typescript +// Partial - Make all properties optional +type PartialContent = Partial; + +// Pick - Select specific properties +type ContentSummary = Pick; + +// Omit - Remove specific properties +type ContentWithoutId = Omit; + +// Readonly - Make immutable +type ReadonlyContent = Readonly; +``` + +### Comments and Documentation + +**When to Comment**: +- Explain "why" not "what" +- Document complex algorithms +- JSDoc for public APIs +- Warn about gotchas or non-obvious behavior + +**JSDoc for Web Components**: + +```typescript +/** + * A button component that triggers document actions. + * @element umb-document-action-button + * @fires {CustomEvent} action-click - Fired when action is clicked + * @slot - Default slot for button content + * @cssprop --umb-button-color - Button text color + */ +export class UmbDocumentActionButton extends LitElement { + /** + * The action identifier + * @attr + * @type {string} + */ + @property({ type: String }) + action = ''; +} +``` + +**TODOs**: + +```typescript +// TODO: Implement pagination [NL] +// FIXME: Memory leak in subscription [JOV] +// HACK: Temporary workaround for API bug [LK] +``` + +**Remove Dead Code**: +- Don't comment out code, delete it (Git history preserves it) +- Remove unused imports, functions, variables +- Clean up console.logs before committing + +### Patterns to Avoid + +**Don't**: +- Use `var` (use `const`/`let`) +- Modify prototypes of built-in objects +- Use global variables +- Block the main thread (use web workers for heavy computation) +- Create deeply nested structures +- Use `any` type (use `unknown` or proper types) +- Use non-null assertions `!` unless absolutely necessary +- Ignore TypeScript errors with `@ts-ignore` + + diff --git a/src/Umbraco.Web.UI.Client/docs/commands.md b/src/Umbraco.Web.UI.Client/docs/commands.md new file mode 100644 index 000000000000..9da597320c52 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/commands.md @@ -0,0 +1,214 @@ +# Commands +[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md) + +--- + + +### Installation + +```bash +# Install dependencies (must use npm, not yarn/pnpm due to workspaces) +npm install + +# Note: Requires Node >=22.17.1 and npm >=10.9.2 +``` + +### Build Commands + +```bash +# TypeScript compilation only +npm run build + +# Build for CMS (production build + copy to .NET project) +npm run build:for:cms + +# Build for npm distribution (with type declarations) +npm run build:for:npm + +# Build with Vite (alternative build method) +npm run build:vite + +# Build workspaces +npm run build:workspaces + +# Build Storybook documentation +npm run build-storybook +``` + +### Development Commands + +```bash +# Start dev server (with live reload) +npm run dev + +# Start dev server connected to real backend +npm run dev:server + +# Start dev server with MSW mocks (default) +npm run dev:mock + +# Preview production build +npm run preview +``` + +### Test Commands + +```bash +# Run all unit tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests in development mode +npm run test:dev + +# Run tests in watch mode (dev config) +npm run test:dev-watch + +# Run E2E tests with Playwright +npm run test:e2e + +# Run example tests +npm run test:examples + +# Run example tests in watch mode +npm run test:examples:watch + +# Run example tests in browser +npm run test:examples:browser +``` + +### Code Quality Commands + +```bash +# Lint TypeScript files +npm run lint + +# Lint and show only errors +npm run lint:errors + +# Lint and auto-fix issues +npm run lint:fix + +# Format code +npm run format + +# Format and auto-fix +npm run format:fix + +# Type check +npm run compile + +# Run all checks (lint, compile, build-storybook, jsonschema) +npm run check +``` + +### Code Generation Commands + +```bash +# Generate TypeScript config +npm run generate:tsconfig + +# Generate OpenAPI client from backend API +npm run generate:server-api + +# Generate icons +npm run generate:icons + +# Generate package manifest +npm run generate:manifest + +# Generate JSON schema for umbraco-package.json +npm run generate:jsonschema + +# Generate JSON schema to dist +npm run generate:jsonschema:dist + +# Generate UI API docs with TypeDoc +npm run generate:ui-api-docs + +# Generate const check tests +npm run generate:check-const-test +``` + +### Analysis Commands + +```bash +# Check for circular dependencies +npm run check:circular + +# Check module dependencies +npm run check:module-dependencies + +# Check path lengths +npm run check:paths + +# Analyze web components +npm run wc-analyze + +# Analyze web components for VS Code +npm run wc-analyze:vscode +``` + +### Storybook Commands + +```bash +# Start Storybook dev server +npm run storybook + +# Build Storybook +npm run storybook:build + +# Build and preview Storybook +npm run storybook:preview +``` + +### Package Management + +```bash +# Validate package exports +npm run package:validate + +# Prepare for npm publish +npm run prepack +``` + +### Environment Setup + +**Prerequisites**: +- Node.js >=22.17.1 +- npm >=10.9.2 +- Modern browser (Chrome, Firefox, Safari) + +**Initial Setup**: + +1. Clone repository + ```bash + git clone https://github.com/umbraco/Umbraco-CMS.git + cd Umbraco-CMS/src/Umbraco.Web.UI.Client + ``` + +2. Install dependencies + ```bash + npm install + ``` + +3. Configure environment (optional) + ```bash + cp .env .env.local + # Edit .env.local with your settings + ``` + +4. Start development + ```bash + npm run dev + ``` + +**Environment Variables** (see `.env` file): + +- `VITE_UMBRACO_USE_MSW` - Enable/disable Mock Service Worker (`on`/`off`) +- `VITE_UMBRACO_API_URL` - Backend API URL (e.g., `https://localhost:44339`) +- `VITE_UMBRACO_INSTALL_STATUS` - Install status (`running`, `must-install`, `must-upgrade`) +- `VITE_UMBRACO_EXTENSION_MOCKS` - Enable extension mocks (`on`/`off`) + diff --git a/src/Umbraco.Web.UI.Client/docs/edge-cases.md b/src/Umbraco.Web.UI.Client/docs/edge-cases.md new file mode 100644 index 000000000000..581b18e3ee36 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/edge-cases.md @@ -0,0 +1,584 @@ +# Edge Cases +[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md) + +--- + + +### Null/Undefined Handling + +**Always Check**: + +```typescript +// Use optional chaining +const name = this._content?.name; + +// Use nullish coalescing for defaults +const name = this._content?.name ?? 'Untitled'; + +// Check before accessing +if (!this._content) { + return html`

No content

`; +} +``` + +**TypeScript Strict Null Checks** (enabled): +- Variables are non-nullable by default +- Use `Type | undefined` or `Type | null` explicitly +- TypeScript forces null checks + +**Function Parameters**: + +```typescript +// Make nullable parameters explicit +function load(id: string, culture?: string) { + const cultureCode = culture ?? 'en-US'; // Default value +} +``` + +### Array Edge Cases + +**Empty Arrays**: + +```typescript +// Check length before access +if (this._items.length === 0) { + return html`

No items

`; +} + +// Safe array methods +const first = this._items[0]; // Could be undefined +const first = this._items.at(0); // Also could be undefined + +// Use optional chaining +const firstId = this._items[0]?.id; +``` + +**Single vs Multiple Items**: + +```typescript +// Handle both cases +const items = Array.isArray(data) ? data : [data]; +``` + +**Array Methods on Undefined**: + +```typescript +// Guard against undefined +const ids = this._items?.map(item => item.id) ?? []; + +// Or check first +if (!this._items) { + return []; +} +return this._items.map(item => item.id); +``` + +**Sparse Arrays** (rare in this codebase): + +```typescript +// Use filter to remove empty slots +const dense = sparse.filter(() => true); +``` + +### String Edge Cases + +**Empty Strings**: + +```typescript +// Check for empty strings +if (!name || name.trim().length === 0) { + return 'Untitled'; +} + +// Or use default +const displayName = name?.trim() || 'Untitled'; +``` + +**String vs Null/Undefined**: + +```typescript +// Distinguish between empty string and null +const hasValue = value !== null && value !== undefined; +const isEmpty = value === ''; + +// Or use optional chaining +const length = value?.length ?? 0; +``` + +**Trim Whitespace**: + +```typescript +// Always trim user input +const cleanName = this._name.trim(); + +// Validate after trimming +if (cleanName.length === 0) { + // Invalid +} +``` + +**String Encoding**: +- Use UTF-8 everywhere +- Be aware of Unicode characters (emojis, etc.) +- Use `textContent` not `innerHTML` for plain text + +**Internationalization**: + +```typescript +// Use localization API +const label = this.localize.term('general_submit'); + +// Not hardcoded strings +// const label = 'Submit'; // ❌ +``` + +### Number Edge Cases + +**NaN Checks**: + +```typescript +// Use Number.isNaN, not isNaN +if (Number.isNaN(value)) { + // Handle NaN +} + +// isNaN coerces, Number.isNaN doesn't +isNaN('hello'); // true (coerces to NaN) +Number.isNaN('hello'); // false (not a number) +``` + +**Infinity**: + +```typescript +if (!Number.isFinite(value)) { + // Handle Infinity or NaN +} +``` + +**Parsing**: + +```typescript +// parseInt/parseFloat can return NaN +const num = parseInt(input, 10); +if (Number.isNaN(num)) { + // Handle invalid input +} + +// Or use Number constructor with validation +const num = Number(input); +if (!Number.isFinite(num)) { + // Invalid +} +``` + +**Floating Point Precision**: + +```typescript +// Don't compare floats with === +const isEqual = Math.abs(a - b) < 0.0001; + +// Or use integers for currency (cents, not dollars) +const priceInCents = 1099; // $10.99 +``` + +**Division by Zero**: + +```typescript +// JavaScript returns Infinity, not error +const result = 10 / 0; // Infinity + +// Check denominator +if (denominator === 0) { + // Handle division by zero + return 0; // Or throw error +} +``` + +**Safe Integer Range**: + +```typescript +// JavaScript integers are safe up to Number.MAX_SAFE_INTEGER +const isSafe = Number.isSafeInteger(value); + +// For IDs, use strings not numbers +interface UmbContentModel { + id: string; // Not number +} +``` + +### Object Edge Cases + +**Property Existence**: + +```typescript +// Use optional chaining +const value = obj?.property; + +// Or check explicitly +if ('property' in obj) { + const value = obj.property; +} + +// hasOwnProperty (not inherited) +if (Object.hasOwn(obj, 'property')) { + // Property exists on object itself +} +``` + +**Null vs Undefined vs {}**: + +```typescript +// Distinguish between missing and empty +const isEmpty = obj !== null && obj !== undefined && Object.keys(obj).length === 0; + +// Or use optional chaining +const hasData = obj && Object.keys(obj).length > 0; +``` + +**Shallow vs Deep Copy**: + +```typescript +// Shallow copy +const copy = { ...original }; + +// Deep copy (for simple objects) +const deepCopy = JSON.parse(JSON.stringify(original)); + +// Deep copy with structuredClone (modern browsers) +const deepCopy = structuredClone(original); + +// Note: Functions, symbols, and undefined are not copied by JSON.stringify +``` + +**Object Freezing**: + +```typescript +// Prevent modification +const frozen = Object.freeze(obj); + +// Check if frozen +if (Object.isFrozen(obj)) { + // Can't modify +} +``` + +### Async/Await Edge Cases + +**Unhandled Promise Rejections**: + +```typescript +// Always catch errors +try { + await asyncOperation(); +} catch (error) { + console.error('Failed:', error); +} + +// Or use .catch() +asyncOperation().catch(error => { + console.error('Failed:', error); +}); + +// For fire-and-forget, explicitly catch +void asyncOperation().catch(error => console.error(error)); +``` + +**Promise.all Fails Fast**: + +```typescript +// Promise.all rejects if ANY promise rejects +try { + const results = await Promise.all([op1(), op2(), op3()]); +} catch (error) { + // One failed, others may still be running +} + +// Use Promise.allSettled to wait for all (even if some fail) +const results = await Promise.allSettled([op1(), op2(), op3()]); +results.forEach((result, index) => { + if (result.status === 'fulfilled') { + console.log(`Op ${index} succeeded:`, result.value); + } else { + console.error(`Op ${index} failed:`, result.reason); + } +}); +``` + +**Race Conditions**: + +```typescript +// Avoid race conditions with sequential operations +this._loading = true; +try { + const data1 = await fetchData1(); + const data2 = await fetchData2(data1.id); + this._result = processData(data1, data2); +} finally { + this._loading = false; +} + +// For parallel operations, use Promise.all +const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]); +``` + +**Timeout Handling**: + +```typescript +// Implement timeout for operations +function withTimeout(promise: Promise, ms: number): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), ms) + ), + ]); +} + +// Usage +try { + const data = await withTimeout(fetchData(), 5000); +} catch (error) { + // Handle timeout or other errors +} +``` + +**Memory Leaks with Event Listeners**: + +```typescript +export class UmbMyElement extends LitElement { + #controller = new UmbMyController(this); + + // Controllers automatically clean up on disconnect + constructor() { + super(); + this.#controller.observe(dataSource$, (value) => { + this._data = value; + }); + } + + // Lit lifecycle handles this automatically + disconnectedCallback() { + super.disconnectedCallback(); + // Controllers are destroyed automatically + } +} +``` + +### Web Component Edge Cases + +**Custom Element Not Defined**: + +```typescript +// Check if element is defined +if (!customElements.get('umb-my-element')) { + // Not defined yet + await customElements.whenDefined('umb-my-element'); +} + +// Or use upgrade +await customElements.upgrade(element); +``` + +**Shadow DOM Queries**: + +```typescript +// Query shadow root, not document +const button = this.shadowRoot?.querySelector('button'); + +// Use Lit decorators +@query('#myButton') +private _button!: HTMLButtonElement; +``` + +**Property vs Attribute Sync**: + +```typescript +// Lit keeps properties and attributes in sync +@property({ type: String }) +name = ''; // Syncs with name="" attribute + +// But complex types don't sync to attributes +@property({ type: Object }) +data = {}; // No attribute sync + +// State doesn't sync to attributes +@state() +private _loading = false; +``` + +**Reactive Update Timing**: + +```typescript +// Wait for update to complete +this.value = 'new value'; +await this.updateComplete; +// Now DOM is updated + +// Or use requestUpdate +this.requestUpdate(); +await this.updateComplete; +``` + +### Date/Time Edge Cases + +**Use Luxon** (not Date): + +```typescript +import { DateTime } from '@umbraco-cms/backoffice/external/luxon'; + +// Create dates +const now = DateTime.now(); +const utc = DateTime.utc(); +const parsed = DateTime.fromISO('2024-01-15T10:30:00Z'); + +// Always store dates in UTC +const isoString = now.toUTC().toISO(); + +// Format for display +const formatted = now.toLocaleString(DateTime.DATETIME_MED); + +// Timezone handling +const local = utc.setZone('local'); +``` + +**Date Comparison**: + +```typescript +// Compare DateTime objects +if (date1 < date2) { } +if (date1.equals(date2)) { } + +// Or compare timestamps +if (date1.toMillis() < date2.toMillis()) { } +``` + +**Date Parsing Can Fail**: + +```typescript +const date = DateTime.fromISO(input); +if (!date.isValid) { + console.error('Invalid date:', date.invalidReason); + // Handle invalid date +} +``` + +### JSON Edge Cases + +**JSON.parse Can Throw**: + +```typescript +// Always wrap in try/catch +try { + const data = JSON.parse(jsonString); +} catch (error) { + console.error('Invalid JSON:', error); + // Handle parse error +} +``` + +**Circular References**: + +```typescript +// JSON.stringify throws on circular references +const obj = { a: 1 }; +obj.self = obj; + +try { + JSON.stringify(obj); +} catch (error) { + // TypeError: Converting circular structure to JSON +} +``` + +**Date Objects**: + +```typescript +// Dates become strings +const data = { date: new Date() }; +const json = JSON.stringify(data); +const parsed = JSON.parse(json); +// parsed.date is a string, not Date + +// Use ISO format explicitly +const isoDate = new Date().toISOString(); +``` + +**Undefined Values**: + +```typescript +// Undefined values are omitted +const obj = { a: 1, b: undefined }; +JSON.stringify(obj); // '{"a":1}' + +// Use null for explicit absence +const obj = { a: 1, b: null }; +JSON.stringify(obj); // '{"a":1,"b":null}' +``` + +### Handling Strategy + +**Guard Clauses**: + +```typescript +// Check preconditions early +if (!this._data) return; +if (this._data.length === 0) return; +if (!this._data[0].name) return; + +// Now can safely use data +this.processData(this._data[0].name); +``` + +**Defensive Programming**: + +```typescript +// Validate inputs +function process(items: unknown) { + if (!Array.isArray(items)) { + throw new Error('Expected array'); + } + // Safe to use items as array +} + +// Use type guards +if (isContentModel(data)) { + // TypeScript knows data is UmbContentModel +} +``` + +**Fail Fast**: + +```typescript +// Throw errors early for programmer mistakes +if (this._repository === undefined) { + throw new Error('Repository not initialized'); +} + +// Handle expected errors gracefully +try { + const data = await this._repository.loadById(id); +} catch (error) { + this._error = 'Failed to load content'; + return; +} +``` + +**Document Edge Cases**: + +```typescript +/** + * Loads content by ID. + * @throws {UmbContentNotFoundError} If content doesn't exist + * @throws {UmbUnauthorizedError} If user lacks permission + * @returns {Promise} The content model + * + * @remarks + * This method returns cached data if available. + * Pass `{ skipCache: true }` to force a fresh load. + */ +async loadById(id: string, options?: LoadOptions): Promise { + // ... +} +``` + + diff --git a/src/Umbraco.Web.UI.Client/docs/error-handling.md b/src/Umbraco.Web.UI.Client/docs/error-handling.md new file mode 100644 index 000000000000..562d3fc244bf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/error-handling.md @@ -0,0 +1,242 @@ +# Error Handling +[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md) + +--- + + +### Diagnosis Process + +1. **Check browser console** for errors and warnings +2. **Reproduce consistently** - Identify exact steps +3. **Check network tab** for failed API calls +4. **Inspect element** to verify DOM structure +5. **Check Lit reactive update cycle** - Use `element.updateComplete` +6. **Verify context availability** - Check context providers +7. **Review event listeners** - Check event propagation +8. **Use browser debugger** with source maps + +**Common Web Component Issues**: +- Component not rendering: Check if custom element is defined +- Properties not updating: Verify `@property()` decorator and reactive update cycle +- Events not firing: Check event names and listeners +- Shadow DOM issues: Use `shadowRoot.querySelector()` not `querySelector()` +- Styles not applied: Check Shadow DOM style encapsulation +- Context not available: Ensure context provider is ancestor in DOM tree + +### Error Handling Standards + +**Error Classes**: + +```typescript +// Use built-in Error class or extend it +throw new Error('Failed to load content'); + +// Custom error classes for domain errors +export class UmbContentNotFoundError extends Error { + constructor(id: string) { + super(`Content with id "${id}" not found`); + this.name = 'UmbContentNotFoundError'; + } +} +``` + +**Repository Pattern Error Handling**: + +```typescript +async requestById(id: string): Promise<{ data?: UmbContentModel; error?: Error }> { + try { + const response = await this._apiClient.getById({ id }); + return { data: response.data }; + } catch (error) { + return { error: error as Error }; + } +} + +// Usage +const { data, error } = await repository.requestById('123'); +if (error) { + // Handle error + console.error('Failed to load content:', error); + return; +} +// Use data +``` + +**Observable Error Handling**: + +```typescript +this.observe(dataSource$, (value) => { + // Success handler + this._data = value; +}).catch((error) => { + // Error handler + console.error('Observable error:', error); +}); +``` + +**Promise Error Handling**: + +```typescript +// Always use try/catch with async/await +async myMethod() { + try { + const result = await this.fetchData(); + return result; + } catch (error) { + console.error('Failed to fetch data:', error); + throw error; // Re-throw or handle + } +} + +// Or use .catch() +this.fetchData() + .then(result => this.handleResult(result)) + .catch(error => this.handleError(error)); +``` + +### Web Component Error Handling + +**Lifecycle Errors**: + +```typescript +export class UmbMyElement extends UmbElementMixin(LitElement) { + constructor() { + try { + super(); + // Initialization that might throw + } catch (error) { + console.error('Failed to initialize element:', error); + } + } + + async connectedCallback() { + try { + super.connectedCallback(); + // Async initialization + await this.loadData(); + } catch (error) { + console.error('Failed to connect element:', error); + this._errorMessage = 'Failed to load component'; + } + } +} +``` + +**Render Errors**: + +```typescript +override render() { + if (this._error) { + return html``; + } + + if (!this._data) { + return html``; + } + + return html` + + `; +} +``` + +### Logging Standards + +**Console Logging**: + +```typescript +// Development only - Remove before production +console.log('Debug info:', data); + +// Errors - Kept in production but sanitized +console.error('Failed to load:', error); + +// Warnings +console.warn('Deprecated API usage:', method); + +// Avoid console.log in production code (ESLint warning) +``` + +**Custom Logging** (if needed): + +```typescript +// Use debug flag for verbose logging +if (this._debug) { + console.log('[UmbMyComponent]', 'State changed:', this._state); +} +``` + +**Don't Log Sensitive Data**: +- User credentials +- API tokens +- Personal information (PII) +- Session IDs +- Full error stack traces in production + +### Development Environment + +- **Source Maps**: Enabled in Vite config for debugging +- **Error Overlay**: Vite provides error overlay in development +- **Hot Module Replacement**: Instant feedback on code changes +- **Detailed Errors**: Full stack traces with source locations +- **TypeScript Checking**: Real-time type checking in IDE + +### Production Environment + +- **Sanitized Errors**: No stack traces exposed to users +- **User-Friendly Messages**: Show helpful error messages +- **Error Boundaries**: Catch errors at component boundaries +- **Graceful Degradation**: Fallback UI when errors occur +- **Error Reporting**: Log errors to console (browser dev tools) + +**Production Error Display**: + +```typescript +private _errorMessage?: string; + +override render() { + if (this._errorMessage) { + return html` + +

${this._errorMessage}

+ +
+ `; + } + // ... normal render +} +``` + +### Context-Specific Error Handling + +**HTTP Client Errors** (OpenAPI): + +```typescript +try { + const response = await this._apiClient.getDocument({ id }); + return response.data; +} catch (error) { + if (error.status === 404) { + throw new UmbContentNotFoundError(id); + } + if (error.status === 403) { + throw new UmbUnauthorizedError('Access denied'); + } + throw error; +} +``` + +**Observable Subscription Errors**: + +```typescript +this._subscription = this._dataSource.asObservable().subscribe({ + next: (value) => this._data = value, + error: (error) => { + console.error('Observable error:', error); + this._errorMessage = 'Failed to load data'; + }, + complete: () => console.log('Observable completed'), +}); +``` + + diff --git a/src/Umbraco.Web.UI.Client/docs/security.md b/src/Umbraco.Web.UI.Client/docs/security.md new file mode 100644 index 000000000000..d3ea55f9029b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/security.md @@ -0,0 +1,299 @@ +# Security +[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md) + +--- + + +### Input Validation + +**Validate All User Input**: + +```typescript +// Use validation in forms +import { UmbValidationController } from '@umbraco-cms/backoffice/validation'; + +#validation = new UmbValidationController(this); + +async #handleSubmit() { + if (!this.#validation.validate()) { + return; // Show validation errors + } + // Proceed with submission +} +``` + +**String Validation**: + +```typescript +private _validateName(name: string): boolean { + // Length check + if (name.length === 0 || name.length > 100) { + return false; + } + + // Pattern check (example: alphanumeric and spaces) + if (!/^[a-zA-Z0-9\s]+$/.test(name)) { + return false; + } + + return true; +} +``` + +**Sanitize HTML**: + +```typescript +// Use DOMPurify for HTML sanitization +import DOMPurify from '@umbraco-cms/backoffice/external/dompurify'; + +const cleanHtml = DOMPurify.sanitize(userInput); + +// In Lit templates, use unsafeHTML directive with sanitized content +import { unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; + +render() { + return html`
${unsafeHTML(DOMPurify.sanitize(this.htmlContent))}
`; +} +``` + +### Authentication & Authorization + +**OpenID Connect** via backend: +- Backoffice uses OpenID Connect for authentication +- Authentication handled by .NET backend +- Tokens managed by browser (httpOnly cookies) + +**Authorization Checks**: + +```typescript +// Check user permissions before actions +#authContext?: UmbAuthContext; + +async #handleDelete() { + const hasPermission = await this.#authContext?.hasPermission('delete'); + if (!hasPermission) { + // Show error or hide action + return; + } + // Proceed with deletion +} +``` + +**Context Security**: +- Use Context API for auth state +- Don't store sensitive tokens in localStorage +- Backend handles token refresh + +### API Security + +**HTTP Client Security**: + +```typescript +// OpenAPI client handles: +// - CSRF tokens +// - Request headers +// - Credentials +// - Error handling + +// Use generated OpenAPI client +import { ContentResource } from '@umbraco-cms/backoffice/external/backend-api'; + +const client = new ContentResource(); +const response = await client.getById({ id }); +``` + +**CORS** (Backend Configuration): +- Configured in .NET backend +- Backoffice follows same-origin policy +- API calls to same origin + +**Rate Limiting** (Backend): +- Handled by .NET backend +- Backoffice respects rate limit headers + +### XSS Prevention + +**Template Security** (Lit): + +```typescript +// Lit automatically escapes content in templates +render() { + // Safe - Automatically escaped + return html`
${this.userContent}
`; + + // UNSAFE - Only use with sanitized content + return html`
${unsafeHTML(DOMPurify.sanitize(this.htmlContent))}
`; +} +``` + +**Attribute Binding**: + +```typescript +// Safe - Lit escapes attribute values +render() { + return html``; +} +``` + +**Event Handlers**: + +```typescript +// Safe - Event handlers are not strings +render() { + return html``; +} + +// NEVER do this (code injection risk) +// render() { +// return html``; +// } +``` + +### Content Security Policy + +**CSP Headers** (Backend Configuration): +- Configured in .NET backend +- Restricts script sources +- Prevents inline scripts (except with nonce) +- Reports violations + +**Backoffice Compliance**: +- No inline scripts +- No `eval()` or `Function()` constructor +- Monaco Editor uses web workers (CSP compliant) + +### Dependencies Security + +**Package Management**: + +```bash +# Check for vulnerabilities +npm audit + +# Fix automatically +npm audit fix + +# Update dependencies carefully +npm update +``` + +**Dependency Security Practices**: +- Renovate bot automatically creates PRs for updates +- Review dependency changes before merging +- Only use packages from npm registry +- Verify package integrity +- Keep dependencies updated + +**Known Vulnerabilities**: +- CI checks for vulnerabilities on every PR +- Security advisories reviewed regularly + +### Common Vulnerabilities + +**XSS (Cross-Site Scripting)**: +- ✅ Lit templates automatically escape content +- ✅ DOMPurify for HTML sanitization +- ❌ Never use `unsafeHTML` with user input directly +- ❌ Never set `innerHTML` with user input + +**CSRF (Cross-Site Request Forgery)**: +- ✅ Backend sends CSRF tokens +- ✅ OpenAPI client includes tokens automatically +- ✅ SameSite cookies + +**Injection Attacks**: +- ✅ Backend uses parameterized queries +- ✅ Input validation on both frontend and backend +- ✅ OpenAPI client prevents injection + +**Prototype Pollution**: +- ❌ Never use `Object.assign` with user input as source +- ❌ Never use `_.merge` with untrusted data +- ✅ Validate object shapes before using + +**ReDoS (Regular Expression Denial of Service)**: +- ✅ Review complex regex patterns +- ✅ Test regex with long inputs +- ❌ Avoid backtracking in regex + +### Secure Coding Practices + +**Don't Trust Client Data**: +- Validate on backend (primary defense) +- Frontend validation is UX, not security + +**Principle of Least Privilege**: +- Only request permissions needed +- Check permissions before sensitive operations +- Hide UI for unavailable actions + +**Sanitize Output**: +- Always sanitize HTML before rendering +- Escape special characters in user content +- Use Lit's automatic escaping + +**Secure Defaults**: +- Forms should validate by default +- Sensitive operations require confirmation +- Errors don't expose sensitive information + +**Defense in Depth**: +- Multiple layers of security +- Frontend validation + Backend validation +- Input sanitization + Output escaping +- Authentication + Authorization + +### Security Anti-Patterns to Avoid + +❌ **Never do this**: +```typescript +// XSS vulnerability +element.innerHTML = userInput; + +// Code injection +eval(userCode); + +// Exposing sensitive data +console.log('Token:', authToken); + +// Storing secrets +localStorage.setItem('apiKey', key); + +// Disabling validation +// @ts-ignore +// eslint-disable-next-line + +// Trusting user input +const url = userInput; // Could be javascript:alert() +window.location.href = url; +``` + +✅ **Do this instead**: +```typescript +// Safe HTML rendering +element.textContent = userInput; +// or +render() { + return html`
${userInput}
`; +} + +// No eval needed +// Use proper JavaScript patterns + +// Don't log sensitive data +console.log('Operation completed'); + +// Backend manages secrets +// Frontend receives tokens via httpOnly cookies + +// Fix TypeScript/ESLint issues properly +// Don't suppress warnings + +// Validate and sanitize URLs +const url = new URL(userInput, window.location.origin); +if (url.protocol === 'https:' || url.protocol === 'http:') { + window.location.href = url.href; +} +``` + + diff --git a/src/Umbraco.Web.UI.Client/docs/style-guide.md b/src/Umbraco.Web.UI.Client/docs/style-guide.md new file mode 100644 index 000000000000..e1c65dd3df5a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/style-guide.md @@ -0,0 +1,157 @@ +# Style Guide +[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md) + +--- + + +### Naming Conventions + +**Files**: +- Web components: `my-component.element.ts` +- Tests: `my-component.test.ts` +- Stories: `my-component.stories.ts` +- Controllers: `my-component.controller.ts` +- Contexts: `my-component.context.ts` +- Modals: `my-component.modal.ts` +- Workspaces: `my-component.workspace.ts` +- Repositories: `my-component.repository.ts` +- Index files: `index.ts` (barrel exports) + +**Classes & Types**: +- Classes: `PascalCase` with `Umb` prefix: `UmbMyComponent` +- Interfaces: `PascalCase` with `Umb` prefix: `UmbMyInterface` +- Types: `PascalCase` with `Umb`, `Ufm`, `Manifest`, `Meta`, or `Example` prefix +- Exported types MUST have approved prefix +- Example types for docs: `ExampleMyType` + +**Variables & Functions**: +- Public members: `camelCase` without underscore: `myVariable`, `myMethod` +- Private members: `camelCase` with leading underscore: `_myPrivateVariable` +- #private members: `camelCase` without underscore: `#myPrivateField` +- Protected members: `camelCase` with optional underscore: `myProtected` or `_myProtected` +- Constants (exported): `UPPER_SNAKE_CASE` with `UMB_` prefix: `UMB_MY_CONSTANT` +- Local constants: `UPPER_CASE` or `camelCase` + +**Custom Elements**: +- Element tag names: kebab-case with `umb-` prefix: `umb-my-component` +- Must be registered in global `HTMLElementTagNameMap` + +### File Organization + +- One class/component per file +- Use barrel exports (`index.ts`) for package public APIs +- Import order (enforced by ESLint): + 1. External dependencies + 2. Parent imports + 3. Sibling imports + 4. Index imports + 5. Type-only imports (separate) + +### Code Formatting (Prettier) + +```json +{ + "printWidth": 120, + "singleQuote": true, + "semi": true, + "bracketSpacing": true, + "bracketSameLine": true, + "useTabs": true +} +``` + +- **Indentation**: Tabs (not spaces) +- **Line length**: 120 characters max +- **Quotes**: Single quotes +- **Semicolons**: Required +- **Trailing commas**: Yes (default) + +### TypeScript Conventions + +**Strict Mode** (enabled in `tsconfig.json`): +- `strict: true` +- `noImplicitReturns: true` +- `noFallthroughCasesInSwitch: true` +- `noImplicitOverride: true` + +**Type Features**: +- Use TypeScript types over JSDoc when possible +- BUT: Lit components use JSDoc for web-component-analyzer compatibility +- Use `type` for unions/intersections: `type MyType = A | B` +- Use `interface` for object shapes and extension: `interface MyInterface extends Base` +- Prefer `const` over `let`, never use `var` +- Use `readonly` for immutable properties +- Use generics for reusable code +- Avoid `any` (lint warning), use `unknown` instead +- Use type guards for narrowing +- Use `as const` for literal types + +**Module Syntax**: +- ES Modules only: `import`/`export` +- Use consistent type imports: `import type { MyType } from '...'` +- Use consistent type exports: `export type { MyType }` +- No side-effects in imports + +**Decorators**: +- `@customElement('umb-my-element')` - Register custom element +- `@property({ type: String })` - Reactive properties +- `@state()` - Internal reactive state +- `@query('#myId')` - Query shadow DOM +- Experimental decorators enabled + +### Modern TypeScript Features to Use + +- **Async/await** over callbacks +- **Optional chaining**: `obj?.property?.method?.()` +- **Nullish coalescing**: `value ?? defaultValue` +- **Template literals**: `` `Hello ${name}` `` +- **Destructuring**: `const { a, b } = obj` +- **Spread operator**: `{ ...obj, newProp: value }` +- **Arrow functions**: `const fn = () => {}` +- **Array methods**: `map`, `filter`, `reduce`, `find`, `some`, `every` +- **Object methods**: `Object.keys`, `Object.values`, `Object.entries` +- **Private fields**: `#privateField` + +### Language Features to Avoid + +- `var` (use `const`/`let`) +- `eval()` or `Function()` constructor +- `with` statement +- `arguments` object (use rest parameters) +- Deeply nested callbacks (use async/await) +- Non-null assertions unless absolutely necessary: `value!` +- Type assertions unless necessary: `value as Type` +- `@ts-ignore` (use `@ts-expect-error` with comment) + +### Custom ESLint Rules + +**Project-Specific Rules**: +- `prefer-static-styles-last` - Static styles property must be last in class +- `enforce-umbraco-external-imports` - External dependencies must be imported via `@umbraco-cms/backoffice/external/*` +- Private members MUST have leading underscore +- Exported types MUST have approved prefix (Umb, Ufm, Manifest, Meta, Example) +- Exported string constants MUST have UMB_ prefix +- Semicolons required +- No `var` keyword +- No circular dependencies (max depth 6) +- No self-imports +- Consistent type imports/exports + +**Allowed JSDoc Tags** (for web-component-analyzer): +- `@element` - Element name +- `@attr` - HTML attribute +- `@fires` - Custom events +- `@prop` - Properties +- `@slot` - Slots +- `@cssprop` - CSS custom properties +- `@csspart` - CSS parts + +### Documentation + +- **Public APIs**: JSDoc comments with `@description`, `@param`, `@returns`, `@example` +- **Web Components**: JSDoc with web-component-analyzer tags +- **Complex logic**: Inline comments explaining "why" not "what" +- **TODOs**: Format as `// TODO: description [initials]` +- **Deprecated**: Use `@deprecated` tag with migration instructions + + diff --git a/src/Umbraco.Web.UI.Client/docs/testing.md b/src/Umbraco.Web.UI.Client/docs/testing.md new file mode 100644 index 000000000000..b1ed63ee05b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/testing.md @@ -0,0 +1,279 @@ +# Testing +[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md) + +--- + + +### Testing Philosophy + +- **Unit tests** for business logic and utilities +- **Component tests** for web components +- **Integration tests** for workflows +- **E2E tests** for critical user journeys +- **Coverage target**: No strict requirement, focus on meaningful tests +- **Test pyramid**: Many unit tests, fewer integration tests, few E2E tests + +### Testing Frameworks + +**Unit/Component Testing**: +- `@web/test-runner` - Fast test runner for web components +- `@open-wc/testing` - Testing utilities, includes Chai assertions +- `@types/chai` - Assertion library +- `@types/mocha` - Test framework (used by @web/test-runner) +- `@web/test-runner-playwright` - Browser launcher +- `element-internals-polyfill` - Polyfill for form-associated custom elements + +**E2E Testing**: +- `@playwright/test` - End-to-end testing in real browsers +- Playwright MSW integration for API mocking + +**Test Utilities**: +- `@umbraco-cms/internal/test-utils` - Shared test utilities +- MSW (Mock Service Worker) - API mocking +- Fixtures in `src/mocks/data/` - Test data + +### Test Project Organization + +``` +src/ +├── **/*.test.ts # Unit tests co-located with source +├── mocks/ +│ ├── data/ # Mock data & in-memory databases +│ └── handlers/ # MSW request handlers +├── examples/ +│ └── **/*.test.ts # Example tests +└── utils/ + └── test-utils.ts # Shared test utilities + +e2e/ +├── **/*.spec.ts # Playwright E2E tests +└── fixtures/ # E2E test fixtures +``` + +### Test Naming Convention + +```typescript +describe('UmbMyComponent', () => { + describe('initialization', () => { + it('should create element', async () => { + // test + }); + + it('should set default properties', async () => { + // test + }); + }); + + describe('user interactions', () => { + it('should emit event when button clicked', async () => { + // test + }); + }); +}); +``` + +### Test Structure (AAA Pattern) + +```typescript +it('should do something', async () => { + // Arrange - Set up test data and conditions + const element = await fixture(html``); + const spy = sinon.spy(); + element.addEventListener('change', spy); + + // Act - Perform the action + element.value = 'new value'; + await element.updateComplete; + + // Assert - Verify the results + expect(spy.calledOnce).to.be.true; + expect(element.value).to.equal('new value'); +}); +``` + +### Unit Test Guidelines + +**What to Test**: +- Public API methods and properties +- User interactions (clicks, inputs, etc.) +- State changes +- Event emissions +- Error handling +- Edge cases + +**What NOT to Test**: +- Private implementation details +- Framework/library code +- Generated code (e.g., OpenAPI clients) +- Third-party dependencies + +**Best Practices**: +- One assertion per test (when practical) +- Test behavior, not implementation +- Use meaningful test names (describe what should happen) +- Keep tests fast (<100ms each) +- Isolate tests (no shared state between tests) +- Use fixtures for DOM elements +- Use spies/stubs for external dependencies +- Clean up after tests (auto-handled by @web/test-runner) + +**Web Component Testing**: + +```typescript +import { fixture, html, expect } from '@open-wc/testing'; +import { UmbMyElement } from './my-element.element'; + +describe('UmbMyElement', () => { + let element: UmbMyElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('should render', () => { + expect(element).to.exist; + expect(element.shadowRoot).to.exist; + }); + + it('should be accessible', async () => { + await expect(element).to.be.accessible(); + }); +}); +``` + +### Integration Test Guidelines + +Integration tests verify interactions between multiple components/systems: + +```typescript +describe('UmbContentRepository', () => { + let repository: UmbContentRepository; + let mockContext: UmbMockContext; + + beforeEach(() => { + mockContext = new UmbMockContext(); + repository = new UmbContentRepository(mockContext); + }); + + it('should fetch and cache content', async () => { + const result = await repository.requestById('content-123'); + expect(result.data).to.exist; + // Verify caching behavior + const cached = await repository.requestById('content-123'); + expect(cached.data).to.equal(result.data); + }); +}); +``` + +### E2E Test Guidelines + +E2E tests with Playwright: + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('Content Editor', () => { + test('should create new document', async ({ page }) => { + await page.goto('/'); + await page.click('[data-test="create-document"]'); + await page.fill('[data-test="document-name"]', 'My New Page'); + await page.click('[data-test="save"]'); + + await expect(page.locator('[data-test="success-notification"]')).toBeVisible(); + }); +}); +``` + +### Mocking Best Practices + +**MSW (Mock Service Worker)** for API mocking: + +```typescript +import { rest } from 'msw'; + +export const handlers = [ + rest.get('/umbraco/management/api/v1/document/:id', (req, res, ctx) => { + const { id } = req.params; + return res( + ctx.json({ + id, + name: 'Test Document', + // ... mock data + }) + ); + }), +]; +``` + +**Context Mocking**: + +```typescript +import { UmbMockContext } from '@umbraco-cms/internal/test-utils'; + +const mockContext = new UmbMockContext(); +mockContext.provideContext(UMB_AUTH_CONTEXT, mockAuthContext); +``` + +### Running Tests + +**Local Development**: + +```bash +# Run all tests once +npm test + +# Run in watch mode +npm run test:watch + +# Run with dev config (faster, less strict) +npm run test:dev + +# Run in watch mode with dev config +npm run test:dev-watch + +# Run specific test file pattern +npm test -- --files "**/my-component.test.ts" +``` + +**E2E Tests**: + +```bash +# Run E2E tests +npm run test:e2e + +# Run in headed mode (see browser) +npx playwright test --headed + +# Run specific test +npx playwright test e2e/content-editor.spec.ts + +# Debug mode +npx playwright test --debug +``` + +**CI/CD**: +- All tests run on pull requests +- E2E tests run on Chromium in CI +- Retries: 2 attempts on CI, 0 locally +- Parallel: Sequential in CI, parallel locally + +### Coverage + +Coverage reporting is currently disabled (see `web-test-runner.config.mjs`): + +```javascript +/* TODO: fix coverage report +coverageConfig: { + reporters: ['lcovonly', 'text-summary'], +}, +*/ +``` + +**What to Exclude from Coverage**: +- Test files themselves +- Mock data and handlers +- Generated code (OpenAPI clients, icons) +- External wrapper modules +- Type declaration files + +