diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5620643..e2e3752 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,24 @@ name: CI on: push: branches: [main] + paths: + - '**/*.cs' + - '**/*.csproj' + - '**/*.sln' + - '**/Directory.Build.props' + - '**/Directory.Packages.props' + - '.github/workflows/ci.yml' + - '.config/dotnet-tools.json' pull_request: branches: [main] + paths: + - '**/*.cs' + - '**/*.csproj' + - '**/*.sln' + - '**/Directory.Build.props' + - '**/Directory.Packages.props' + - '.github/workflows/ci.yml' + - '.config/dotnet-tools.json' workflow_dispatch: env: @@ -13,6 +29,68 @@ env: DOTNET_CLI_TELEMETRY_OPTOUT: true jobs: + # Detect which areas changed to conditionally run tests + changes: + name: Detect Changes + runs-on: ubuntu-latest + outputs: + dataprovider: ${{ steps.filter.outputs.dataprovider }} + lql: ${{ steps.filter.outputs.lql }} + migration: ${{ steps.filter.outputs.migration }} + sync: ${{ steps.filter.outputs.sync }} + gatekeeper: ${{ steps.filter.outputs.gatekeeper }} + samples: ${{ steps.filter.outputs.samples }} + dashboard: ${{ steps.filter.outputs.dashboard }} + steps: + - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + dataprovider: + - 'DataProvider/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + lql: + - 'Lql/**' + - 'DataProvider/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + migration: + - 'Migration/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + sync: + - 'Sync/**' + - 'Migration/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + gatekeeper: + - 'Gatekeeper/**' + - 'DataProvider/**' + - 'Migration/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + samples: + - 'Samples/**' + - 'DataProvider/**' + - 'Sync/**' + - 'Migration/**' + - 'Gatekeeper/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + dashboard: + - 'Samples/Dashboard/**' + - 'Samples/Clinical/**' + - 'Samples/Scheduling/**' + - 'DataProvider/**' + - 'Sync/**' + - 'Migration/**' + - 'Gatekeeper/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + build: name: Build runs-on: ubuntu-latest @@ -46,22 +124,59 @@ jobs: - name: Build run: dotnet build --no-restore -c Release - # Tests that only need SQLite (no Docker) - unit-tests: - name: Unit Tests + # DataProvider tests + dataprovider-tests: + name: DataProvider Tests runs-on: ubuntu-latest - needs: build + needs: [build, changes] + if: needs.changes.outputs.dataprovider == 'true' strategy: fail-fast: false matrix: project: - DataProvider/DataProvider.Tests - DataProvider/DataProvider.Example.Tests + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore + run: dotnet restore ${{ matrix.project }} + + - name: Test + run: dotnet test ${{ matrix.project }} --no-restore --verbosity normal --logger "trx;LogFileName=test-results.trx" + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-dataprovider-${{ strategy.job-index }} + path: '**/TestResults/*.trx' + + # LQL tests + lql-tests: + name: LQL Tests + runs-on: ubuntu-latest + needs: [build, changes] + if: needs.changes.outputs.lql == 'true' + strategy: + fail-fast: false + matrix: + project: - Lql/Lql.Tests - Lql/LqlCli.SQLite.Tests - - Migration/Migration.Tests - - Sync/Sync.Tests - - Sync/Sync.SQLite.Tests steps: - uses: actions/checkout@v4 @@ -88,34 +203,63 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: test-results-unit-${{ strategy.job-index }} + name: test-results-lql-${{ strategy.job-index }} path: '**/TestResults/*.trx' - # API integration tests (SQLite, no Docker) - api-tests: - name: API Tests + # Migration tests + migration-tests: + name: Migration Tests runs-on: ubuntu-latest - needs: build + needs: [build, changes] + if: needs.changes.outputs.migration == 'true' + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore + run: dotnet restore Migration/Migration.Tests + + - name: Test + run: dotnet test Migration/Migration.Tests --no-restore --verbosity normal --logger "trx;LogFileName=test-results.trx" + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-migration + path: '**/TestResults/*.trx' + + # Sync SQLite tests (no Docker) + sync-sqlite-tests: + name: Sync SQLite Tests + runs-on: ubuntu-latest + needs: [build, changes] + if: needs.changes.outputs.sync == 'true' strategy: fail-fast: false matrix: project: - - Gatekeeper/Gatekeeper.Api.Tests - - Samples/Clinical/Clinical.Api.Tests - - Samples/Scheduling/Scheduling.Api.Tests - - Samples/Dashboard/Dashboard.Web.Tests + - Sync/Sync.Tests + - Sync/Sync.SQLite.Tests steps: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: | - 8.0.x - ${{ env.DOTNET_VERSION }} - - - name: Restore .NET tools - run: dotnet tool restore + dotnet-version: ${{ env.DOTNET_VERSION }} - name: Cache NuGet packages uses: actions/cache@v4 @@ -135,14 +279,15 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: test-results-api-${{ strategy.job-index }} + name: test-results-sync-sqlite-${{ strategy.job-index }} path: '**/TestResults/*.trx' - # Tests that need Docker for Postgres (via Testcontainers) - postgres-tests: - name: Postgres Tests + # Sync Postgres tests (need Docker) + sync-postgres-tests: + name: Sync Postgres Tests runs-on: ubuntu-latest - needs: build + needs: [build, changes] + if: needs.changes.outputs.sync == 'true' strategy: fail-fast: false matrix: @@ -172,21 +317,103 @@ jobs: - name: Test run: dotnet test ${{ matrix.project }} --no-restore --verbosity normal --logger "trx;LogFileName=test-results.trx" env: - # Testcontainers will automatically use the Docker daemon TESTCONTAINERS_RYUK_DISABLED: false - name: Upload test results uses: actions/upload-artifact@v4 if: always() with: - name: test-results-postgres-${{ strategy.job-index }} + name: test-results-sync-postgres-${{ strategy.job-index }} + path: '**/TestResults/*.trx' + + # Gatekeeper API tests + gatekeeper-tests: + name: Gatekeeper Tests + runs-on: ubuntu-latest + needs: [build, changes] + if: needs.changes.outputs.gatekeeper == 'true' + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore + run: dotnet restore Gatekeeper/Gatekeeper.Api.Tests + + - name: Test + run: dotnet test Gatekeeper/Gatekeeper.Api.Tests --no-restore --verbosity normal --logger "trx;LogFileName=test-results.trx" + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-gatekeeper + path: '**/TestResults/*.trx' + + # Sample API tests + sample-api-tests: + name: Sample API Tests + runs-on: ubuntu-latest + needs: [build, changes] + if: needs.changes.outputs.samples == 'true' + strategy: + fail-fast: false + matrix: + project: + - Samples/Clinical/Clinical.Api.Tests + - Samples/Scheduling/Scheduling.Api.Tests + - Samples/Dashboard/Dashboard.Web.Tests + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + ${{ env.DOTNET_VERSION }} + + - name: Restore .NET tools + run: dotnet tool restore + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore + run: dotnet restore ${{ matrix.project }} + + - name: Test + run: dotnet test ${{ matrix.project }} --no-restore --verbosity normal --logger "trx;LogFileName=test-results.trx" + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-sample-api-${{ strategy.job-index }} path: '**/TestResults/*.trx' # Dashboard E2E tests (need Playwright browser) e2e-tests: name: Dashboard E2E Tests runs-on: ubuntu-latest - needs: build + needs: [build, changes] + if: needs.changes.outputs.dashboard == 'true' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml new file mode 100644 index 0000000..91981ec --- /dev/null +++ b/.github/workflows/deploy-website.yml @@ -0,0 +1,66 @@ +name: Deploy Website to GitHub Pages + +on: + push: + branches: [main] + paths: + - 'Website/**' + - 'DataProvider/**' + - 'Lql/**' + - '.github/workflows/deploy-website.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: Website/package-lock.json + + - name: Install docfx + run: dotnet tool install -g docfx + + - name: Generate API metadata with docfx + working-directory: Website/docfx + run: docfx metadata docfx.json + + - name: Install npm dependencies + working-directory: Website + run: npm ci + + - name: Generate API docs markdown from DocFX YAML + working-directory: Website + run: node scripts/generate-api-docs.cjs + + - name: Build Eleventy site + working-directory: Website + run: npm run build + + - name: Deploy to DataProviderWebsite repo + uses: peaceiris/actions-gh-pages@v4 + with: + deploy_key: ${{ secrets.DATAPROVIDER_WEBSITE_DEPLOY_KEY }} + external_repository: MelbourneDeveloper/DataProviderWebsite + publish_branch: gh-pages + publish_dir: ./Website/_site diff --git a/.gitignore b/.gitignore index 6e39749..38eb736 100644 --- a/.gitignore +++ b/.gitignore @@ -388,4 +388,14 @@ Samples/Dashboard/Dashboard.Web/wwwroot/js/vendor/ # Dashboard compiled JS (H5/Bridge output) Samples/Dashboard/Dashboard.Web/wwwroot/js/Dashboard.js Samples/Dashboard/Dashboard.Web/wwwroot/js/Dashboard.min.js -Samples/Dashboard/Dashboard.Web/wwwroot/js/index.html \ No newline at end of file +Samples/Dashboard/Dashboard.Web/wwwroot/js/index.html +Website/_site/ + +Website/docfx/api/ + +Website/src/apidocs/ + +# Docs copied from README files (source of truth is in the project READMEs) +Website/src/docs/sync.md +Website/src/docs/dataprovider.md +Website/src/docs/lql.md diff --git a/CLAUDE.md b/CLAUDE.md index 71596ef..f2c9726 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,10 @@ public abstract partial record Result { private Result() { } - **No commented code** - Delete it - **No placeholders** - Leave compile errors with TODO +## CSS +- **MINIMAL CSS** - Do not duplicate CSS clases +- **Name classes after component, NOT section** - Sections should not have their own CSS classes + ## Testing - E2E with zero mocking - 100% coverage, Stryker score 70%+ diff --git a/DataProvider/DataProvider/DataProviderConfig.cs b/DataProvider/DataProvider/DataProviderConfig.cs index 1784176..8d2afdb 100644 --- a/DataProvider/DataProvider/DataProviderConfig.cs +++ b/DataProvider/DataProvider/DataProviderConfig.cs @@ -3,6 +3,20 @@ namespace DataProvider; /// /// Configuration for DataProvider code generation /// +/// +/// +/// // Configure tables for code generation +/// var config = new DataProviderConfig +/// { +/// ConnectionString = "Data Source=app.db", +/// Tables = new List<TableConfig> +/// { +/// new TableConfig { Schema = "main", Name = "users", GenerateDelete = true }, +/// new TableConfig { Schema = "main", Name = "orders" } +/// }.AsReadOnly() +/// }; +/// +/// public sealed record DataProviderConfig { /// @@ -19,6 +33,21 @@ public sealed record DataProviderConfig /// /// Configuration for a single table /// +/// +/// +/// // Configure a table with custom settings +/// var tableConfig = new TableConfig +/// { +/// Schema = "dbo", +/// Name = "Patients", +/// GenerateInsert = true, +/// GenerateUpdate = true, +/// GenerateDelete = false, +/// ExcludeColumns = new List<string> { "computed_column" }.AsReadOnly(), +/// PrimaryKeyColumns = new List<string> { "Id" }.AsReadOnly() +/// }; +/// +/// public sealed record TableConfig { /// diff --git a/DataProvider/DataProvider/DbConnectionExtensions.cs b/DataProvider/DataProvider/DbConnectionExtensions.cs index 73b3993..25fa74d 100644 --- a/DataProvider/DataProvider/DbConnectionExtensions.cs +++ b/DataProvider/DataProvider/DbConnectionExtensions.cs @@ -5,12 +5,36 @@ namespace DataProvider; /// -/// Static extension methods for IDbConnection following FP patterns +/// Static extension methods for IDbConnection following FP patterns. +/// All methods return Result types for explicit error handling. /// +/// +/// +/// using var connection = new SqliteConnection("Data Source=:memory:"); +/// connection.Open(); +/// +/// // Execute a query with mapping +/// var result = connection.Query<Customer>( +/// sql: "SELECT Id, Name FROM Customers WHERE Active = 1", +/// mapper: reader => new Customer( +/// Id: reader.GetInt32(0), +/// Name: reader.GetString(1) +/// ) +/// ); +/// +/// // Pattern match on the result +/// var customers = result switch +/// { +/// Result<IReadOnlyList<Customer>, SqlError>.Ok<IReadOnlyList<Customer>, SqlError> ok => ok.Value, +/// Result<IReadOnlyList<Customer>, SqlError>.Error<IReadOnlyList<Customer>, SqlError> err => throw new Exception(err.Value.Message), +/// _ => throw new InvalidOperationException() +/// }; +/// +/// public static class DbConnectionExtensions { /// - /// Execute a query and return results + /// Execute a query and return results. /// /// The result type /// The database connection @@ -18,6 +42,15 @@ public static class DbConnectionExtensions /// Optional parameters /// Function to map from IDataReader to T /// Result with list of T or error + /// + /// + /// var result = connection.Query<Product>( + /// sql: "SELECT * FROM Products WHERE Price > @minPrice", + /// parameters: [new SqliteParameter("@minPrice", 10.00)], + /// mapper: r => new Product(r.GetInt32(0), r.GetString(1), r.GetDecimal(2)) + /// ); + /// + /// public static Result, SqlError> Query( this IDbConnection connection, string sql, @@ -72,12 +105,26 @@ public static Result, SqlError> Query( } /// - /// Execute a non-query command + /// Execute a non-query command (INSERT, UPDATE, DELETE). /// /// The database connection /// The SQL command /// Optional parameters /// Result with rows affected or error + /// + /// + /// var result = connection.Execute( + /// sql: "UPDATE Products SET Price = @price WHERE Id = @id", + /// parameters: [ + /// new SqliteParameter("@price", 19.99), + /// new SqliteParameter("@id", 42) + /// ] + /// ); + /// + /// if (result is Result<int, SqlError>.Ok<int, SqlError> ok) + /// Console.WriteLine($"Updated {ok.Value} rows"); + /// + /// public static Result Execute( this IDbConnection connection, string sql, diff --git a/DataProvider/DataProvider/DbTransact.cs b/DataProvider/DataProvider/DbTransact.cs index 8001921..149d049 100644 --- a/DataProvider/DataProvider/DbTransact.cs +++ b/DataProvider/DataProvider/DbTransact.cs @@ -7,6 +7,24 @@ namespace DataProvider; /// Provides transactional helpers as extension methods for . /// Opens a transaction, executes the provided delegate, and commits or rolls back accordingly. /// +/// +/// +/// // Execute multiple operations in a transaction +/// await connection.Transact(async tx => +/// { +/// await tx.InsertUserAsync(new User { Id = Guid.NewGuid(), Name = "John" }); +/// await tx.InsertOrderAsync(new Order { UserId = userId, Total = 99.99m }); +/// }); +/// +/// // Execute with a return value +/// var userId = await connection.Transact(async tx => +/// { +/// var user = new User { Id = Guid.NewGuid(), Name = "Jane" }; +/// await tx.InsertUserAsync(user); +/// return user.Id; +/// }); +/// +/// public static class DbTransact { /// diff --git a/DataProvider/DataProvider/SchemaTypes.cs b/DataProvider/DataProvider/SchemaTypes.cs index edad385..a622609 100644 --- a/DataProvider/DataProvider/SchemaTypes.cs +++ b/DataProvider/DataProvider/SchemaTypes.cs @@ -6,6 +6,31 @@ namespace DataProvider; /// /// Represents a database column with its metadata /// +/// +/// +/// // Create a column definition +/// var column = new DatabaseColumn +/// { +/// Name = "Id", +/// SqlType = "uniqueidentifier", +/// CSharpType = "Guid", +/// IsNullable = false, +/// IsPrimaryKey = true, +/// IsIdentity = false, +/// IsComputed = false +/// }; +/// +/// // Create a string column with max length +/// var nameColumn = new DatabaseColumn +/// { +/// Name = "Name", +/// SqlType = "nvarchar(100)", +/// CSharpType = "string", +/// IsNullable = false, +/// MaxLength = 100 +/// }; +/// +/// public sealed record DatabaseColumn { /// @@ -62,6 +87,27 @@ public sealed record DatabaseColumn /// /// Represents a database table with its columns /// +/// +/// +/// // Create a table definition with columns +/// var table = new DatabaseTable +/// { +/// Schema = "dbo", +/// Name = "Patients", +/// Columns = new List<DatabaseColumn> +/// { +/// new DatabaseColumn { Name = "Id", SqlType = "uniqueidentifier", IsPrimaryKey = true }, +/// new DatabaseColumn { Name = "Name", SqlType = "nvarchar(100)" }, +/// new DatabaseColumn { Name = "DateOfBirth", SqlType = "date", IsNullable = true } +/// }.AsReadOnly() +/// }; +/// +/// // Access computed column lists +/// var pkColumns = table.PrimaryKeyColumns; +/// var insertColumns = table.InsertableColumns; +/// var updateColumns = table.UpdateableColumns; +/// +/// public sealed record DatabaseTable { /// diff --git a/DataProvider/README.md b/DataProvider/README.md index 46b12ca..db48df1 100644 --- a/DataProvider/README.md +++ b/DataProvider/README.md @@ -7,7 +7,7 @@ A .NET source generator that creates compile-time safe database extension method - **Compile-Time Safety** - SQL queries are validated during compilation, catching errors before runtime - **Auto-Generated Extensions** - Creates extension methods on `IDbConnection` and `IDbTransaction` - **Schema Inspection** - Automatically inspects database schema to generate appropriate types -- **Result Type Pattern** - All operations return `Result` types for explicit error handling +- **Result Type Pattern** - All operations return `Result` types for explicit error handling - **Multi-Database Support** - Currently supports SQLite and SQL Server - **LQL Integration** - Seamlessly works with Lambda Query Language files @@ -244,7 +244,7 @@ For complex result sets with joins, configure grouping in a `.grouping.json` fil DataProvider follows functional programming principles: - **No Classes** - Uses records and static extension methods -- **No Exceptions** - Returns `Result` types for all operations +- **No Exceptions** - Returns `Result` types for all operations - **Pure Functions** - Static methods with no side effects - **Expression-Based** - Prefers expressions over statements @@ -277,7 +277,7 @@ dotnet test DataProvider.Tests/DataProvider.Tests.csproj ## Error Handling -All methods return `Result` types: +All methods return `Result` types: ```csharp var result = await connection.ExecuteQueryAsync(); diff --git a/Migration/Migration/SchemaBuilder.cs b/Migration/Migration/SchemaBuilder.cs index 18af9be..6d2fb56 100644 --- a/Migration/Migration/SchemaBuilder.cs +++ b/Migration/Migration/SchemaBuilder.cs @@ -3,11 +3,29 @@ namespace Migration; /// /// Fluent builder for schema definitions. /// +/// +/// +/// var schema = SchemaFactory.Define("MyDatabase") +/// .Table("users", t => t +/// .Column("id", PortableTypes.Uuid, c => c.PrimaryKey()) +/// .Column("email", PortableTypes.VarChar(255), c => c.NotNull()) +/// .Column("created_at", PortableTypes.DateTime, c => c.DefaultLql("now()")) +/// .Index("idx_users_email", "email", unique: true)) +/// .Table("orders", t => t +/// .Column("id", PortableTypes.Int, c => c.Identity().PrimaryKey()) +/// .Column("user_id", PortableTypes.Uuid, c => c.NotNull()) +/// .Column("total", PortableTypes.Decimal(10, 2)) +/// .ForeignKey("user_id", "users", "id", onDelete: ForeignKeyAction.Cascade)) +/// .Build(); +/// +/// public static class SchemaFactory { /// /// Start defining a schema with the given name. /// + /// The name of the schema/database. + /// A for fluent configuration. public static SchemaBuilder Define(string name) => new(name); } @@ -28,8 +46,20 @@ public static class Schema } /// -/// Builder for creating schema definitions. +/// Builder for creating schema definitions with tables, columns, indexes, and constraints. /// +/// +/// +/// var schema = Schema.Define("inventory") +/// .Table("products", t => t +/// .Column("id", PortableTypes.Int, c => c.Identity().PrimaryKey()) +/// .Column("name", PortableTypes.VarChar(100), c => c.NotNull()) +/// .Column("price", PortableTypes.Decimal(10, 2), c => c.NotNull().Check("price >= 0")) +/// .Column("sku", PortableTypes.VarChar(50), c => c.NotNull()) +/// .Unique("uq_products_sku", "sku")) +/// .Build(); +/// +/// public sealed class SchemaBuilder { private readonly string _name; @@ -66,8 +96,23 @@ public SchemaBuilder Table(string schema, string name, Action conf } /// -/// Builder for creating table definitions. +/// Builder for creating table definitions with columns, indexes, foreign keys, and constraints. /// +/// +/// +/// // Define a table with various column types and constraints +/// .Table("employees", t => t +/// .Column("id", PortableTypes.Uuid, c => c.PrimaryKey().DefaultLql("gen_uuid()")) +/// .Column("name", PortableTypes.VarChar(100), c => c.NotNull()) +/// .Column("email", PortableTypes.VarChar(255), c => c.NotNull()) +/// .Column("department_id", PortableTypes.Int) +/// .Column("salary", PortableTypes.Decimal(12, 2)) +/// .Column("hired_at", PortableTypes.DateTime, c => c.DefaultLql("now()")) +/// .Index("idx_employees_email", "email", unique: true) +/// .Index("idx_employees_dept", "department_id") +/// .ForeignKey("department_id", "departments", "id")) +/// +/// public sealed class TableBuilder { private readonly string _name; @@ -279,8 +324,26 @@ internal TableDefinition Build() => } /// -/// Builder for creating column definitions. +/// Builder for creating column definitions with type, nullability, defaults, and constraints. /// +/// +/// +/// // UUID primary key with auto-generation +/// .Column("id", PortableTypes.Uuid, c => c.PrimaryKey().DefaultLql("gen_uuid()")) +/// +/// // Required string with max length +/// .Column("name", PortableTypes.VarChar(100), c => c.NotNull()) +/// +/// // Auto-increment integer +/// .Column("sequence", PortableTypes.Int, c => c.Identity()) +/// +/// // Decimal with precision and check constraint +/// .Column("price", PortableTypes.Decimal(10, 2), c => c.NotNull().Check("price > 0")) +/// +/// // DateTime with default to current timestamp +/// .Column("created_at", PortableTypes.DateTime, c => c.DefaultLql("now()")) +/// +/// public sealed class ColumnBuilder { private readonly string _name; diff --git a/Migration/Migration/SchemaDiff.cs b/Migration/Migration/SchemaDiff.cs index 0cae609..97e732e 100644 --- a/Migration/Migration/SchemaDiff.cs +++ b/Migration/Migration/SchemaDiff.cs @@ -4,6 +4,33 @@ namespace Migration; /// Calculates the difference between two schema definitions. /// Produces a list of operations to transform current schema into desired schema. /// +/// +/// +/// // Compare current database schema against desired schema +/// var currentSchema = await schemaInspector.InspectAsync(connection); +/// var desiredSchema = Schema.Define("mydb") +/// .Table("users", t => t +/// .Column("id", PortableTypes.Uuid, c => c.PrimaryKey()) +/// .Column("email", PortableTypes.VarChar(255), c => c.NotNull()) +/// .Column("name", PortableTypes.VarChar(100))) // New column +/// .Build(); +/// +/// // Calculate safe (additive-only) migration operations +/// var result = SchemaDiff.Calculate(currentSchema, desiredSchema); +/// if (result is OperationsResult.Ok<IReadOnlyList<SchemaOperation>, MigrationError> ok) +/// { +/// foreach (var op in ok.Value) +/// { +/// var ddl = ddlGenerator.Generate(op); +/// await connection.ExecuteAsync(ddl); +/// } +/// } +/// +/// // Or allow destructive changes (DROP operations) +/// var destructiveResult = SchemaDiff.Calculate( +/// currentSchema, desiredSchema, allowDestructive: true); +/// +/// public static class SchemaDiff { /// diff --git a/README.md b/README.md index c5cdc8a..85dbac2 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,7 @@ DataProvider/ │ ├── Scheduling/ # FHIR-compliant scheduling API │ └── Dashboard/ # React/H5 dashboard ├── Other/ -│ ├── Results/ # Functional Result type implementation +│ ├── Results/ # Functional Result type implementation │ └── Selecta/ # SQL parsing and AST utilities └── Directory.Build.props # Central build configuration ``` diff --git a/Sync/Sync/SyncBatch.cs b/Sync/Sync/SyncBatch.cs index 7ed0281..fab536c 100644 --- a/Sync/Sync/SyncBatch.cs +++ b/Sync/Sync/SyncBatch.cs @@ -8,6 +8,29 @@ namespace Sync; /// The ending version (inclusive) for this batch. /// True if more batches are available after this one. /// Optional SHA-256 hash of batch contents for verification (spec S15.4). +/// +/// +/// // Create a batch with changes +/// var changes = new List<SyncLogEntry> +/// { +/// new SyncLogEntry(Version: 101, TableName: "patients", ...), +/// new SyncLogEntry(Version: 102, TableName: "appointments", ...), +/// }; +/// var batch = new SyncBatch( +/// Changes: changes, +/// FromVersion: 100, +/// ToVersion: 102, +/// HasMore: true +/// ); +/// +/// // Process batch +/// if (batch.HasMore) +/// { +/// // Fetch next batch starting from ToVersion +/// var nextBatch = await FetchBatch(batch.ToVersion); +/// } +/// +/// public sealed record SyncBatch( IReadOnlyList Changes, long FromVersion, @@ -21,6 +44,24 @@ public sealed record SyncBatch( /// /// Number of records per batch. Default: 1000. /// Max retry passes for deferred FK violations. Default: 3. +/// +/// +/// // Use default configuration (1000 records per batch, 3 retry passes) +/// var defaultConfig = new BatchConfig(); +/// +/// // Custom configuration for large syncs +/// var largeConfig = new BatchConfig(BatchSize: 5000, MaxRetryPasses: 5); +/// +/// // Perform sync with custom config +/// var result = SyncCoordinator.Sync( +/// myOriginId: originId, +/// lastServerVersion: 0, +/// lastPushVersion: 0, +/// config: largeConfig, +/// ... +/// ); +/// +/// public sealed record BatchConfig(int BatchSize = 1000, int MaxRetryPasses = 3); /// @@ -29,4 +70,22 @@ public sealed record BatchConfig(int BatchSize = 1000, int MaxRetryPasses = 3); /// Number of changes successfully applied. /// Number of changes deferred due to FK violations. /// The max version applied in this batch. +/// +/// +/// // After applying a batch +/// var result = new BatchApplyResult( +/// AppliedCount: 95, +/// DeferredCount: 5, +/// ToVersion: 150 +/// ); +/// +/// // Check for deferred changes (FK violations that need retry) +/// if (result.DeferredCount > 0) +/// { +/// Console.WriteLine($"{result.DeferredCount} changes deferred, will retry"); +/// } +/// +/// Console.WriteLine($"Applied {result.AppliedCount} changes up to version {result.ToVersion}"); +/// +/// public sealed record BatchApplyResult(int AppliedCount, int DeferredCount, long ToVersion); diff --git a/Sync/Sync/SyncCoordinator.cs b/Sync/Sync/SyncCoordinator.cs index 3ec7b4d..be4a23c 100644 --- a/Sync/Sync/SyncCoordinator.cs +++ b/Sync/Sync/SyncCoordinator.cs @@ -8,6 +8,13 @@ namespace Sync; /// Number of changes pulled and applied. /// Starting version of the pull. /// Ending version after the pull. +/// +/// +/// // After a successful pull operation +/// var result = new PullResult(ChangesApplied: 42, FromVersion: 100, ToVersion: 142); +/// Console.WriteLine($"Pulled {result.ChangesApplied} changes"); +/// +/// public sealed record PullResult(int ChangesApplied, long FromVersion, long ToVersion); /// @@ -16,6 +23,13 @@ public sealed record PullResult(int ChangesApplied, long FromVersion, long ToVer /// Number of changes pushed. /// Starting version of the push. /// Ending version after the push. +/// +/// +/// // After a successful push operation +/// var result = new PushResult(ChangesPushed: 15, FromVersion: 50, ToVersion: 65); +/// Console.WriteLine($"Pushed {result.ChangesPushed} changes to server"); +/// +/// public sealed record PushResult(int ChangesPushed, long FromVersion, long ToVersion); /// @@ -23,6 +37,16 @@ public sealed record PushResult(int ChangesPushed, long FromVersion, long ToVers /// /// Result of pulling changes from remote. /// Result of pushing changes to remote. +/// +/// +/// // After a full sync operation +/// var syncResult = new SyncResult( +/// Pull: new PullResult(ChangesApplied: 42, FromVersion: 100, ToVersion: 142), +/// Push: new PushResult(ChangesPushed: 15, FromVersion: 50, ToVersion: 65) +/// ); +/// Console.WriteLine($"Sync complete: pulled {syncResult.Pull.ChangesApplied}, pushed {syncResult.Push.ChangesPushed}"); +/// +/// public sealed record SyncResult(PullResult Pull, PushResult Push); /// @@ -30,6 +54,29 @@ public sealed record SyncResult(PullResult Pull, PushResult Push); /// Main entry point for pull/push sync operations. /// Implements spec Section 11 (Bi-Directional Sync Protocol). /// +/// +/// +/// // Perform a bidirectional sync +/// using var connection = new SqliteConnection("Data Source=local.db"); +/// connection.Open(); +/// +/// var result = SyncCoordinator.Sync( +/// myOriginId: "client-uuid-123", +/// lastServerVersion: 100, +/// lastPushVersion: 50, +/// config: new BatchConfig(BatchSize: 1000), +/// fetchRemoteChanges: (version, limit) => httpClient.GetChanges(version, limit), +/// applyLocalChange: entry => ApplyChange(connection, entry), +/// enableTriggerSuppression: () => EnableSuppression(connection), +/// disableTriggerSuppression: () => DisableSuppression(connection), +/// updateLastServerVersion: v => UpdateServerVersion(connection, v), +/// fetchLocalChanges: (version, limit) => GetLocalChanges(connection, version, limit), +/// sendToRemote: entries => httpClient.PushChanges(entries), +/// updateLastPushVersion: v => UpdatePushVersion(connection, v), +/// logger: logger +/// ); +/// +/// public static class SyncCoordinator { /// diff --git a/Sync/Sync/SyncLogEntry.cs b/Sync/Sync/SyncLogEntry.cs index 4d4d0b0..184ed19 100644 --- a/Sync/Sync/SyncLogEntry.cs +++ b/Sync/Sync/SyncLogEntry.cs @@ -11,6 +11,31 @@ namespace Sync; /// JSON-serialized row data. NULL for deletes. /// UUID of the replica that created this change. /// ISO 8601 UTC timestamp with milliseconds. +/// +/// +/// // Create an insert entry for a new patient record +/// var insertEntry = new SyncLogEntry( +/// Version: 1, +/// TableName: "patients", +/// PkValue: "{\"Id\": \"550e8400-e29b-41d4-a716-446655440000\"}", +/// Operation: SyncOperation.Insert, +/// Payload: "{\"Id\": \"550e8400-e29b-41d4-a716-446655440000\", \"Name\": \"John Doe\"}", +/// Origin: "client-uuid-123", +/// Timestamp: DateTime.UtcNow.ToString("o") +/// ); +/// +/// // Create a delete entry (tombstone) +/// var deleteEntry = new SyncLogEntry( +/// Version: 2, +/// TableName: "patients", +/// PkValue: "{\"Id\": \"550e8400-e29b-41d4-a716-446655440000\"}", +/// Operation: SyncOperation.Delete, +/// Payload: null, +/// Origin: "client-uuid-123", +/// Timestamp: DateTime.UtcNow.ToString("o") +/// ); +/// +/// public sealed record SyncLogEntry( long Version, string TableName, diff --git a/Sync/Sync/SyncState.cs b/Sync/Sync/SyncState.cs index 2b833dc..c3277af 100644 --- a/Sync/Sync/SyncState.cs +++ b/Sync/Sync/SyncState.cs @@ -6,12 +6,45 @@ namespace Sync; /// UUID v4 identifying this replica. Generated once, never changes. /// Last version successfully pulled from server. /// Last local version successfully pushed to server. +/// +/// +/// // Initialize sync state for a new client +/// var state = new SyncState( +/// OriginId: Guid.NewGuid().ToString(), +/// LastServerVersion: 0, +/// LastPushVersion: 0 +/// ); +/// +/// // After pulling changes from server +/// var updatedState = state with { LastServerVersion = 142 }; +/// +/// // After pushing changes to server +/// var finalState = updatedState with { LastPushVersion = 65 }; +/// +/// public sealed record SyncState(string OriginId, long LastServerVersion, long LastPushVersion); /// /// Represents the ephemeral sync session state (_sync_session table). /// /// When true (1), triggers are suppressed during change application. +/// +/// +/// // Enable trigger suppression during sync +/// var session = new SyncSession(SyncActive: true); +/// await syncSessionRepository.SetAsync(session); +/// +/// // Apply remote changes without triggering local change logging +/// foreach (var change in remoteChanges) +/// { +/// await ApplyChange(change); +/// } +/// +/// // Disable trigger suppression +/// var inactive = new SyncSession(SyncActive: false); +/// await syncSessionRepository.SetAsync(inactive); +/// +/// public sealed record SyncSession(bool SyncActive); /// @@ -22,6 +55,20 @@ public sealed record SyncSession(bool SyncActive); /// Last version this client has synced to. /// ISO 8601 UTC timestamp of last sync. /// ISO 8601 UTC timestamp when client was first registered. +/// +/// +/// // Register a new sync client +/// var client = new SyncClient( +/// OriginId: Guid.NewGuid().ToString(), +/// LastSyncVersion: 0, +/// LastSyncTimestamp: DateTime.UtcNow.ToString("o"), +/// CreatedAt: DateTime.UtcNow.ToString("o") +/// ); +/// +/// // Store in repository +/// await syncClientRepository.UpsertAsync(client); +/// +/// public sealed record SyncClient( string OriginId, long LastSyncVersion, diff --git a/Website/DataProvider-Design-System.md b/Website/DataProvider-Design-System.md new file mode 100644 index 0000000..861d137 --- /dev/null +++ b/Website/DataProvider-Design-System.md @@ -0,0 +1,131 @@ +# DataProvider Design System + +A clean, technical system for a modern .NET data toolkit. Dark-first, high contrast, no purple. + +--- + +## Brand Principles + +- Precise +- Fast +- Trustworthy +- Developer-first +- Minimal ornamentation + +You optimize for clarity over decoration. + +--- + +## Color Palette + +### Core Colors + +| Name | Hex | Usage | +|-----|-----|------| +| Midnight Blue | `#0B132B` | Primary background | +| Deep Navy | `#101A3A` | Panels, cards | +| Slate Blue | `#1C2A5A` | Borders, dividers | +| Cloud White | `#F4F7FB` | Primary text | +| Muted Gray | `#9AA4BF` | Secondary text | + +### Accent Colors + +| Name | Hex | Usage | +|-----|-----|------| +| Data Green | `#6BCF8E` | Primary actions | +| Signal Blue | `#4DA3FF` | Links, focus | +| Query Orange | `#FF9F43` | LQL, emphasis | + +### Status Colors + +| State | Hex | +|------|-----| +| Success | `#4CAF50` | +| Warning | `#FFB020` | +| Error | `#FF5C5C` | +| Info | `#4DA3FF` | + +--- + +## Typography + +### Font Stack + +Primary: +``` +Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif +``` + +Monospace: +``` +JetBrains Mono, Fira Code, Consolas, monospace +``` + +### Type Scale + +| Role | Size | Weight | Line Height | +|-----|------|--------|-------------| +| H1 | 48px | 700 | 1.15 | +| H2 | 36px | 700 | 1.2 | +| H3 | 28px | 600 | 1.25 | +| H4 | 22px | 600 | 1.3 | +| Body | 16px | 400 | 1.6 | +| Small | 14px | 400 | 1.6 | +| Code | 14px | 400 | 1.5 | + +--- + +## Layout + +- Max width: 1200px +- Content padding: 24px +- Section spacing: 96px +- Card gap: 24px + +--- + +## Components + +### Buttons + +Primary: +- Background: `#6BCF8E` +- Text: `#0B132B` +- Radius: 8px + +Secondary: +- Border: `#4DA3FF` +- Text: `#4DA3FF` + +--- + +## Code Blocks + +- Background: `#0B132B` +- Border: `#1C2A5A` +- Keywords: `#4DA3FF` +- Types: `#6BCF8E` +- Strings: `#FF9F43` +- Comments: `#6B7280` + +--- + +## Accessibility + +- WCAG AA contrast +- Focus outline: `#4DA3FF` +- No color-only meaning + +--- + +## Voice + +Short. Direct. Developer-first. + +--- + +## Don’ts + +- No purple +- No glassmorphism +- No buzzwords diff --git a/Website/Designs/APIDoc.png b/Website/Designs/APIDoc.png new file mode 100644 index 0000000..82d1796 Binary files /dev/null and b/Website/Designs/APIDoc.png differ diff --git a/Website/Designs/BlogsPage.png b/Website/Designs/BlogsPage.png new file mode 100644 index 0000000..eece955 Binary files /dev/null and b/Website/Designs/BlogsPage.png differ diff --git a/Website/Designs/HomePage.png b/Website/Designs/HomePage.png new file mode 100644 index 0000000..1073ffe Binary files /dev/null and b/Website/Designs/HomePage.png differ diff --git a/Website/docfx/docfx.json b/Website/docfx/docfx.json new file mode 100644 index 0000000..4157b2e --- /dev/null +++ b/Website/docfx/docfx.json @@ -0,0 +1,55 @@ +{ + "metadata": [ + { + "src": [ + { + "files": ["DataProvider/DataProvider.csproj", "DataProvider.SQLite/DataProvider.SQLite.csproj", "DataProvider.SqlServer/DataProvider.SqlServer.csproj"], + "src": "../../DataProvider" + }, + { + "files": ["Sync/Sync.csproj", "Sync.SQLite/Sync.SQLite.csproj", "Sync.Postgres/Sync.Postgres.csproj", "Sync.Http/Sync.Http.csproj"], + "src": "../../Sync" + }, + { + "files": ["Migration/Migration.csproj", "Migration.SQLite/Migration.SQLite.csproj", "Migration.Postgres/Migration.Postgres.csproj"], + "src": "../../Migration" + }, + { + "files": ["Gatekeeper.Api/Gatekeeper.Api.csproj"], + "src": "../../Gatekeeper" + } + ], + "dest": "api", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "filter": "filterConfig.yml", + "allowCompilationErrors": true + } + ], + "build": { + "content": [ + { + "files": ["api/**.yml", "api/index.md"] + }, + { + "files": ["toc.yml", "*.md"] + } + ], + "resource": [ + { + "files": ["images/**"] + } + ], + "output": "_apidocs", + "globalMetadataFiles": [], + "fileMetadataFiles": [], + "template": ["default"], + "postProcessors": [], + "keepFileLink": false, + "disableGitFeatures": false + } +} diff --git a/Website/docfx/filterConfig.yml b/Website/docfx/filterConfig.yml new file mode 100644 index 0000000..8f5bb87 --- /dev/null +++ b/Website/docfx/filterConfig.yml @@ -0,0 +1,13 @@ +apiRules: + - exclude: + uidRegex: ^.*\.Tests.*$ + type: Namespace + - exclude: + uidRegex: ^.*\.Example.*$ + type: Namespace + - exclude: + uidRegex: ^.*\.Cli.*$ + type: Namespace + - exclude: + hasAttribute: + uid: System.ObsoleteAttribute diff --git a/Website/docfx/index.md b/Website/docfx/index.md new file mode 100644 index 0000000..6dcbc6d --- /dev/null +++ b/Website/docfx/index.md @@ -0,0 +1,11 @@ +# API Reference + +This is the auto-generated API reference for DataProvider. + +## Namespaces + +- [DataProvider API](api/index.md) - Core DataProvider classes + +## External Documentation + +- [LQL Documentation](https://lql.dev) - Lambda Query Language documentation diff --git a/Website/docfx/toc.yml b/Website/docfx/toc.yml new file mode 100644 index 0000000..e00c56a --- /dev/null +++ b/Website/docfx/toc.yml @@ -0,0 +1,2 @@ +- name: DataProvider API + href: api/ diff --git a/Website/eleventy.config.js b/Website/eleventy.config.js new file mode 100644 index 0000000..0b34c34 --- /dev/null +++ b/Website/eleventy.config.js @@ -0,0 +1,78 @@ +import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight"; +import pluginRss from "@11ty/eleventy-plugin-rss"; +import eleventyNavigationPlugin from "@11ty/eleventy-navigation"; +import markdownIt from "markdown-it"; +import markdownItAnchor from "markdown-it-anchor"; + +export default function(eleventyConfig) { + const mdOptions = { + html: true, + breaks: false, + linkify: true + }; + + const mdAnchorOptions = { + permalink: markdownItAnchor.permalink.headerLink(), + slugify: (s) => s.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, ''), + level: [1, 2, 3, 4] + }; + + const md = markdownIt(mdOptions).use(markdownItAnchor, mdAnchorOptions); + eleventyConfig.setLibrary("md", md); + + eleventyConfig.addPlugin(syntaxHighlight); + eleventyConfig.addPlugin(pluginRss); + eleventyConfig.addPlugin(eleventyNavigationPlugin); + + eleventyConfig.addPassthroughCopy("src/assets"); + eleventyConfig.addPassthroughCopy("src/robots.txt"); + eleventyConfig.addWatchTarget("src/assets/"); + + eleventyConfig.addCollection("posts", function(collectionApi) { + return collectionApi.getFilteredByGlob("src/blog/*.md").sort((a, b) => b.date - a.date); + }); + + eleventyConfig.addCollection("docs", function(collectionApi) { + return collectionApi.getFilteredByGlob("src/docs/**/*.md"); + }); + + eleventyConfig.addCollection("tagList", function(collectionApi) { + const tagSet = new Set(); + collectionApi.getFilteredByGlob("src/blog/*.md").forEach(post => { + (post.data.tags || []).forEach(tag => { + tag !== 'post' && tag !== 'posts' && tagSet.add(tag); + }); + }); + return [...tagSet].sort(); + }); + + eleventyConfig.addCollection("postsByTag", function(collectionApi) { + const postsByTag = {}; + collectionApi.getFilteredByGlob("src/blog/*.md").forEach(post => { + (post.data.tags || []).forEach(tag => { + tag !== 'post' && tag !== 'posts' && (postsByTag[tag] = postsByTag[tag] || []).push(post); + }); + }); + return postsByTag; + }); + + eleventyConfig.addFilter("dateFormat", (dateObj) => { + return new Date(dateObj).toLocaleDateString('en-US', { + year: 'numeric', month: 'long', day: 'numeric' + }); + }); + + eleventyConfig.addFilter("isoDate", (dateObj) => new Date(dateObj).toISOString()); + eleventyConfig.addFilter("limit", (arr, limit) => arr.slice(0, limit)); + eleventyConfig.addFilter("capitalize", (str) => str ? str.charAt(0).toUpperCase() + str.slice(1) : ''); + eleventyConfig.addFilter("slugify", (str) => str ? str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '') : ''); + + eleventyConfig.addShortcode("year", () => String(new Date().getFullYear())); + + return { + dir: { input: "src", output: "_site", includes: "_includes", data: "_data" }, + templateFormats: ["md", "njk", "html"], + markdownTemplateEngine: "njk", + htmlTemplateEngine: "njk" + }; +} diff --git a/Website/images/AlternativeLogo.png b/Website/images/AlternativeLogo.png new file mode 100644 index 0000000..81fece3 Binary files /dev/null and b/Website/images/AlternativeLogo.png differ diff --git a/Website/images/BackgroundImage.png b/Website/images/BackgroundImage.png new file mode 100644 index 0000000..812d7de Binary files /dev/null and b/Website/images/BackgroundImage.png differ diff --git a/Website/images/Logo.png b/Website/images/Logo.png new file mode 100644 index 0000000..608519a Binary files /dev/null and b/Website/images/Logo.png differ diff --git a/Website/images/LogoAndText.png b/Website/images/LogoAndText.png new file mode 100644 index 0000000..dfb18d6 Binary files /dev/null and b/Website/images/LogoAndText.png differ diff --git a/Website/package-lock.json b/Website/package-lock.json new file mode 100644 index 0000000..a404c21 --- /dev/null +++ b/Website/package-lock.json @@ -0,0 +1,1820 @@ +{ + "name": "dataprovider-website", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dataprovider-website", + "version": "1.0.0", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@11ty/eleventy": "^3.1.2", + "@11ty/eleventy-navigation": "^0.3.5", + "@11ty/eleventy-plugin-rss": "^2.0.2", + "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", + "markdown-it-anchor": "^9.2.0" + } + }, + "node_modules/@11ty/dependency-tree": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-4.0.1.tgz", + "integrity": "sha512-6EPI9ZkGU4BX2KNZpWlf4WdV3vrmIWQpn//nAXicTzdPubI3jZlmFdqEv0Yj5M7oavRUGNzw9GbV9cBxhulZWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@11ty/eleventy-utils": "^2.0.1" + } + }, + "node_modules/@11ty/dependency-tree-esm": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@11ty/dependency-tree-esm/-/dependency-tree-esm-2.0.4.tgz", + "integrity": "sha512-MYKC0Ac77ILr1HnRJalzKDlb9Z8To3kXQCltx299pUXXUFtJ1RIONtULlknknqW8cLe19DLVgmxVCtjEFm7h0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@11ty/eleventy-utils": "^2.0.7", + "acorn": "^8.15.0", + "dependency-graph": "^1.0.0", + "normalize-path": "^3.0.0" + } + }, + "node_modules/@11ty/eleventy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.1.2.tgz", + "integrity": "sha512-IcsDlbXnBf8cHzbM1YBv3JcTyLB35EK88QexmVyFdVJVgUU6bh9g687rpxryJirHzo06PuwnYaEEdVZQfIgRGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@11ty/dependency-tree": "^4.0.0", + "@11ty/dependency-tree-esm": "^2.0.0", + "@11ty/eleventy-dev-server": "^2.0.8", + "@11ty/eleventy-plugin-bundle": "^3.0.6", + "@11ty/eleventy-utils": "^2.0.7", + "@11ty/lodash-custom": "^4.17.21", + "@11ty/posthtml-urls": "^1.0.1", + "@11ty/recursive-copy": "^4.0.2", + "@sindresorhus/slugify": "^2.2.1", + "bcp-47-normalize": "^2.3.0", + "chokidar": "^3.6.0", + "debug": "^4.4.1", + "dependency-graph": "^1.0.0", + "entities": "^6.0.1", + "filesize": "^10.1.6", + "gray-matter": "^4.0.3", + "iso-639-1": "^3.1.5", + "js-yaml": "^4.1.0", + "kleur": "^4.1.5", + "liquidjs": "^10.21.1", + "luxon": "^3.6.1", + "markdown-it": "^14.1.0", + "minimist": "^1.2.8", + "moo": "^0.5.2", + "node-retrieve-globals": "^6.0.1", + "nunjucks": "^3.2.4", + "picomatch": "^4.0.2", + "please-upgrade-node": "^3.2.0", + "posthtml": "^0.16.6", + "posthtml-match-helper": "^2.0.3", + "semver": "^7.7.2", + "slugify": "^1.6.6", + "tinyglobby": "^0.2.14" + }, + "bin": { + "eleventy": "cmd.cjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-dev-server": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-dev-server/-/eleventy-dev-server-2.0.8.tgz", + "integrity": "sha512-15oC5M1DQlCaOMUq4limKRYmWiGecDaGwryr7fTE/oM9Ix8siqMvWi+I8VjsfrGr+iViDvWcH/TVI6D12d93mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@11ty/eleventy-utils": "^2.0.1", + "chokidar": "^3.6.0", + "debug": "^4.4.0", + "finalhandler": "^1.3.1", + "mime": "^3.0.0", + "minimist": "^1.2.8", + "morphdom": "^2.7.4", + "please-upgrade-node": "^3.2.0", + "send": "^1.1.0", + "ssri": "^11.0.0", + "urlpattern-polyfill": "^10.0.0", + "ws": "^8.18.1" + }, + "bin": { + "eleventy-dev-server": "cmd.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-navigation": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-navigation/-/eleventy-navigation-0.3.5.tgz", + "integrity": "sha512-4aKW5aIQDFed8xs1G1pWcEiFPcDSwZtA4IH1eERtoJ+Xy+/fsoe0pzbDmw84bHZ9ACny5jblENhfZhcCxklqQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dependency-graph": "^0.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-navigation/node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/@11ty/eleventy-plugin-bundle": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz", + "integrity": "sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@11ty/eleventy-utils": "^2.0.2", + "debug": "^4.4.0", + "posthtml-match-helper": "^2.0.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-plugin-rss": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-rss/-/eleventy-plugin-rss-2.0.4.tgz", + "integrity": "sha512-LF60sGVlxGTryQe3hTifuzrwF8R7XbrNsM2xfcDcNMSliLN4kmB+7zvoLRySRx0AQDjqhPTAeeeT0ra6/9zHUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@11ty/eleventy-utils": "^2.0.0", + "@11ty/posthtml-urls": "^1.0.1", + "debug": "^4.4.0", + "posthtml": "^0.16.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-plugin-syntaxhighlight": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-syntaxhighlight/-/eleventy-plugin-syntaxhighlight-5.0.2.tgz", + "integrity": "sha512-T6xVVRDJuHlrFMHbUiZkHjj5o1IlLzZW+1IL9eUsyXFU7rY2ztcYhZew/64vmceFFpQwzuSfxQOXxTJYmKkQ+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-utils": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-2.0.7.tgz", + "integrity": "sha512-6QE+duqSQ0GY9rENXYb4iPR4AYGdrFpqnmi59tFp9VrleOl0QSh8VlBr2yd6dlhkdtj7904poZW5PvGr9cMiJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/lodash-custom": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@11ty/lodash-custom/-/lodash-custom-4.17.21.tgz", + "integrity": "sha512-Mqt6im1xpb1Ykn3nbcCovWXK3ggywRJa+IXIdoz4wIIK+cvozADH63lexcuPpGS/gJ6/m2JxyyXDyupkMr5DHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/posthtml-urls": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.2.tgz", + "integrity": "sha512-0vaV3Wt0surZ+oS1VdKKe0axeeupuM+l7W/Z866WFQwF+dGg2Tc/nmhk/5l74/Y55P8KyImnLN9CdygNw2huHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "evaluate-value": "^2.0.0", + "http-equiv-refresh": "^2.0.1", + "list-to-array": "^1.1.0", + "parse-srcset": "^1.0.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@11ty/recursive-copy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-4.0.3.tgz", + "integrity": "sha512-SX48BTLEGX8T/OsKWORsHAAeiDsbFl79Oa/0Wg/mv/d27b7trCVZs7fMHvpSgDvZz/fZqx5rDk8+nx5oyT7xBw==", + "dev": true, + "license": "ISC", + "dependencies": { + "errno": "^1.0.0", + "junk": "^3.1.0", + "maximatch": "^0.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sindresorhus/slugify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/transliterate": "^1.0.0", + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcp-47": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", + "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-normalize": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz", + "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bcp-47": "^2.0.0", + "bcp-47-match": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/errno": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/errno/-/errno-1.0.0.tgz", + "integrity": "sha512-3zV5mFS1E8/1bPxt/B0xxzI1snsg3uSCIh6Zo1qKg6iMw93hzPANk9oBFzSFBFrwuVoQuE3rLoouAUfwOAj1wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esm-import-transformer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/esm-import-transformer/-/esm-import-transformer-3.0.5.tgz", + "integrity": "sha512-1GKLvfuMnnpI75l8c6sHoz0L3Z872xL5akGuBudgqTDPv4Vy6f2Ec7jEMKTxlqWl/3kSvNbHELeimJtnqgYniw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/evaluate-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/evaluate-value/-/evaluate-value-2.0.0.tgz", + "integrity": "sha512-VonfiuDJc0z4sOO7W0Pd130VLsXN6vmBWZlrog1mCb/o7o/Nl5Lr25+Kj/nkCCAhG+zqeeGjxhkK9oHpkgTHhQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filesize": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", + "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 10.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-equiv-refresh": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-equiv-refresh/-/http-equiv-refresh-2.0.1.tgz", + "integrity": "sha512-XJpDL/MLkV3dKwLzHwr2dY05dYNfBNlyPu4STQ8WvKCFdc6vC5tPXuq28of663+gHVg03C+16pHHs/+FmmDjcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", + "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/iso-639-1": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz", + "integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/liquidjs": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.24.0.tgz", + "integrity": "sha512-TAUNAdgwaAXjjcUFuYVJm9kOVH7zc0mTKxsG9t9Lu4qdWjB2BEblyVIYpjWcmJLMGgiYqnGNJjpNMHx0gp/46A==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^10.0.0" + }, + "bin": { + "liquid": "bin/liquid.js", + "liquidjs": "bin/liquid.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/liquidjs" + } + }, + "node_modules/list-to-array": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz", + "integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.0.tgz", + "integrity": "sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==", + "dev": true, + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/maximatch": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz", + "integrity": "sha512-9ORVtDUFk4u/NFfo0vG/ND/z7UQCVZBL539YW0+U1I7H1BkZwizcPx5foFv7LCPcBnm2U6RjFnQOsIvN4/Vm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-differ": "^1.0.0", + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "minimatch": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/morphdom": { + "version": "2.7.7", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.7.tgz", + "integrity": "sha512-04GmsiBcalrSCNmzfo+UjU8tt3PhZJKzcOy+r1FlGA7/zri8wre3I1WkYN9PT3sIeIKfW9bpyElA+VzOg2E24g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-retrieve-globals": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-6.0.1.tgz", + "integrity": "sha512-j0DeFuZ/Wg3VlklfbxUgZF/mdHMTEiEipBb3q0SpMMbHaV3AVfoUQF8UGxh1s/yjqO0TgRZd4Pi/x2yRqoQ4Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.1", + "acorn-walk": "^8.3.4", + "esm-import-transformer": "^3.0.3" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nunjucks": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "chokidar": "^3.3.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/nunjucks/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/posthtml": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz", + "integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "posthtml-parser": "^0.11.0", + "posthtml-render": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/posthtml-match-helper": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/posthtml-match-helper/-/posthtml-match-helper-2.0.3.tgz", + "integrity": "sha512-p9oJgTdMF2dyd7WE54QI1LvpBIkNkbSiiECKezNnDVYhGhD1AaOnAkw0Uh0y5TW+OHO8iBdSqnd8Wkpb6iUqmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "posthtml": "^0.16.6" + } + }, + "node_modules/posthtml-parser": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", + "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^7.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/posthtml-render": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", + "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-json": "^2.0.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-11.0.0.tgz", + "integrity": "sha512-aZpUoMN/Jj2MqA4vMCeiKGnc/8SuSyHbGSBdgFbZxP8OJGF/lFkIuElzPxsN0q8TQQ+prw3P4EDfB3TBHHgfXw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/Website/package.json b/Website/package.json new file mode 100644 index 0000000..697376f --- /dev/null +++ b/Website/package.json @@ -0,0 +1,22 @@ +{ + "name": "dataprovider-website", + "version": "1.0.0", + "description": "Documentation website for DataProvider - Effortless .NET Data Access", + "type": "module", + "scripts": { + "dev": "eleventy --serve", + "build": "eleventy", + "build:site": "eleventy", + "generate-api": "node scripts/generate-api-docs.cjs" + }, + "devDependencies": { + "@11ty/eleventy": "^3.1.2", + "@11ty/eleventy-navigation": "^0.3.5", + "@11ty/eleventy-plugin-rss": "^2.0.2", + "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", + "markdown-it-anchor": "^9.2.0" + }, + "dependencies": { + "js-yaml": "^4.1.0" + } +} diff --git a/Website/scripts/generate-api-docs.cjs b/Website/scripts/generate-api-docs.cjs new file mode 100644 index 0000000..793eeaa --- /dev/null +++ b/Website/scripts/generate-api-docs.cjs @@ -0,0 +1,278 @@ +/** + * Generate API documentation markdown from DocFX YAML output + * Converts YAML files to markdown with Eleventy frontmatter + */ + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const DOCFX_API_DIR = path.join(__dirname, '../docfx/api'); +const OUTPUT_DIR = path.join(__dirname, '../src/apidocs'); + +// Ensure output directory exists +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +function escapeHtml(text) { + if (!text) return ''; + return text.replace(/&/g, '&').replace(//g, '>'); +} + +function cleanMarkdown(text) { + if (!text) return ''; + return text + .replace(/]*>([^<]*)<\/xref>/g, '`$2`') + .replace(/]*\/>/g, (_, uid) => '`' + uid.split('.').pop() + '`') + .replace(/([^<]*)<\/see>/g, '`$1`') + .replace(//g, '') + .trim(); +} + +function formatExample(exampleArray) { + if (!exampleArray || !Array.isArray(exampleArray) || exampleArray.length === 0) return ''; + + let md = '\n## Example\n\n'; + for (const example of exampleArray) { + // DocFX wraps examples in
...
+ // Extract the code content and convert to markdown code block + let code = example; + + // Remove
 wrapper if present
+    const codeMatch = code.match(/
]*>([\s\S]*?)<\/code><\/pre>/);
+    if (codeMatch) {
+      code = codeMatch[1];
+    }
+
+    // Unescape HTML entities
+    code = code
+      .replace(/</g, '<')
+      .replace(/>/g, '>')
+      .replace(/&/g, '&')
+      .replace(/"/g, '"')
+      .replace(/'/g, "'");
+
+    md += '```csharp\n' + code.trim() + '\n```\n\n';
+  }
+  return md;
+}
+
+function formatTypeName(type) {
+  if (!type) return '';
+  return type.replace(/\x60\d+/g, '').replace(/\{/g, '<').replace(/\}/g, '>').split('.').pop();
+}
+
+function generateNamespaceMarkdown(doc, types) {
+  const nsTypes = types.filter(t => t.namespace === doc.name);
+  let md = '---\nlayout: layouts/api.njk\ntitle: "' + doc.name + '"\ndescription: "API documentation for ' + doc.name + ' namespace"\nnamespace: "' + doc.name + '"\ntype: "namespace"\n---\n\n';
+  
+  if (doc.summary) md += cleanMarkdown(doc.summary) + '\n\n';
+
+  const classes = nsTypes.filter(i => i.type === 'Class');
+  const records = nsTypes.filter(i => i.type === 'Record');
+  const interfaces = nsTypes.filter(i => i.type === 'Interface');
+  const enums = nsTypes.filter(i => i.type === 'Enum');
+
+  if (classes.length > 0) {
+    md += '## Classes\n\n| Class | Description |\n|-------|-------------|\n';
+    for (const item of classes) {
+      const name = item.name.split('.').pop();
+      const desc = cleanMarkdown(item.summary) || '';
+      md += '| [' + name + '](' + name + '/) | ' + desc.split('\n')[0] + ' |\n';
+    }
+    md += '\n';
+  }
+
+  if (records.length > 0) {
+    md += '## Records\n\n| Record | Description |\n|--------|-------------|\n';
+    for (const item of records) {
+      const name = item.name.split('.').pop();
+      const desc = cleanMarkdown(item.summary) || '';
+      md += '| [' + name + '](' + name + '/) | ' + desc.split('\n')[0] + ' |\n';
+    }
+    md += '\n';
+  }
+
+  if (interfaces.length > 0) {
+    md += '## Interfaces\n\n| Interface | Description |\n|-----------|-------------|\n';
+    for (const item of interfaces) {
+      const name = item.name.split('.').pop();
+      const desc = cleanMarkdown(item.summary) || '';
+      md += '| [' + name + '](' + name + '/) | ' + desc.split('\n')[0] + ' |\n';
+    }
+    md += '\n';
+  }
+
+  if (enums.length > 0) {
+    md += '## Enums\n\n| Enum | Description |\n|------|-------------|\n';
+    for (const item of enums) {
+      const name = item.name.split('.').pop();
+      const desc = cleanMarkdown(item.summary) || '';
+      md += '| [' + name + '](' + name + '/) | ' + desc.split('\n')[0] + ' |\n';
+    }
+    md += '\n';
+  }
+
+  return md;
+}
+
+function generateTypeMarkdown(doc, allItems) {
+  const typeName = doc.name.split('.').pop();
+  const namespace = doc.namespace || doc.name.split('.').slice(0, -1).join('.');
+
+  let md = '---\nlayout: layouts/api.njk\ntitle: "' + typeName + '"\ndescription: "API documentation for ' + typeName + '"\nnamespace: "' + namespace + '"\ntype: "' + (doc.type || 'Type').toLowerCase() + '"\n---\n\n';
+  md += '
Classes > ' + namespace + ' > ' + typeName + '
\n\n'; + + if (doc.summary) md += cleanMarkdown(doc.summary) + '\n\n'; + if (doc.syntax && doc.syntax.content) md += '```csharp\n' + doc.syntax.content + '\n```\n\n'; + + // Add class-level examples + if (doc.example && doc.example.length > 0) { + md += formatExample(doc.example); + } + + const children = allItems.filter(i => i.parent === doc.uid); + const constructors = children.filter(c => c.type === 'Constructor'); + const properties = children.filter(c => c.type === 'Property'); + const methods = children.filter(c => c.type === 'Method'); + const fields = children.filter(c => c.type === 'Field'); + + if (constructors.length > 0) { + md += '## Constructors\n\n'; + for (const ctor of constructors) { + md += '### ' + escapeHtml(typeName) + '\n\n'; + if (ctor.syntax && ctor.syntax.content) md += '```csharp\n' + ctor.syntax.content + '\n```\n\n'; + if (ctor.summary) md += cleanMarkdown(ctor.summary) + '\n\n'; + if (ctor.syntax && ctor.syntax.parameters && ctor.syntax.parameters.length > 0) { + md += '| Parameter | Type | Description |\n|-----------|------|-------------|\n'; + for (const param of ctor.syntax.parameters) { + md += '| `' + param.id + '` | `' + formatTypeName(param.type) + '` | ' + (cleanMarkdown(param.description) || '') + ' |\n'; + } + md += '\n'; + } + } + } + + if (properties.length > 0) { + md += '## Properties\n\n'; + for (const prop of properties) { + const name = prop.name.split('.').pop(); + md += '### ' + name + '\n\n'; + if (prop.syntax && prop.syntax.content) md += '```csharp\n' + prop.syntax.content + '\n```\n\n'; + if (prop.summary) md += cleanMarkdown(prop.summary) + '\n\n'; + } + } + + if (methods.length > 0) { + md += '## Methods\n\n'; + for (const method of methods) { + const name = method.name.split('.').pop(); + md += '### ' + escapeHtml(name) + '\n\n'; + if (method.syntax && method.syntax.content) md += '```csharp\n' + method.syntax.content + '\n```\n\n'; + if (method.summary) md += cleanMarkdown(method.summary) + '\n\n'; + if (method.syntax && method.syntax.parameters && method.syntax.parameters.length > 0) { + md += '**Parameters:**\n\n| Name | Type | Description |\n|------|------|-------------|\n'; + for (const param of method.syntax.parameters) { + md += '| `' + param.id + '` | `' + formatTypeName(param.type) + '` | ' + (cleanMarkdown(param.description) || '') + ' |\n'; + } + md += '\n'; + } + if (method.syntax && method.syntax.return) { + md += '**Returns:** `' + formatTypeName(method.syntax.return.type) + '`'; + if (method.syntax.return.description) md += ' - ' + cleanMarkdown(method.syntax.return.description); + md += '\n\n'; + } + // Add method-level examples + if (method.example && method.example.length > 0) { + md += formatExample(method.example); + } + } + } + + if (fields.length > 0) { + md += '## Values\n\n| Name | Description |\n|------|-------------|\n'; + for (const field of fields) { + const name = field.name.split('.').pop(); + const desc = cleanMarkdown(field.summary) || ''; + md += '| `' + name + '` | ' + desc.split('\n')[0] + ' |\n'; + } + md += '\n'; + } + + return md; +} + +function processYamlFiles() { + if (!fs.existsSync(DOCFX_API_DIR)) { + console.log('DocFX API directory not found at:', DOCFX_API_DIR); + console.log('Run "docfx metadata docfx.json" first.'); + process.exit(1); + } + + const files = fs.readdirSync(DOCFX_API_DIR).filter(f => f.endsWith('.yml') && f !== 'toc.yml'); + if (files.length === 0) { + console.log('No YAML files found.'); + process.exit(1); + } + + const allItems = []; + const namespaces = new Map(); + + for (const file of files) { + const content = fs.readFileSync(path.join(DOCFX_API_DIR, file), 'utf8'); + try { + const docs = yaml.loadAll(content); + for (const doc of docs) { + if (!doc || !doc.items) continue; + for (const item of doc.items) { + allItems.push(item); + if (item.type === 'Namespace') namespaces.set(item.uid, item); + } + } + } catch (e) { + console.error('Error parsing ' + file + ':', e.message); + } + } + + console.log('Found ' + namespaces.size + ' namespaces and ' + allItems.length + ' total items'); + + if (fs.existsSync(OUTPUT_DIR)) fs.rmSync(OUTPUT_DIR, { recursive: true }); + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + + const sortedNamespaces = Array.from(namespaces.values()).sort((a, b) => a.name.localeCompare(b.name)); + let indexMd = '---\nlayout: layouts/api.njk\ntitle: "API Reference"\ndescription: "DataProvider API Reference"\ntype: "index"\n---\n\nAuto-generated API documentation from source code.\n\n## Namespaces\n\n| Namespace | Description |\n|-----------|-------------|\n'; + + for (const ns of sortedNamespaces) { + const summary = cleanMarkdown(ns.summary) || ''; + const nsPath = ns.name.replace(/\./g, '/'); + indexMd += '| [' + ns.name + '](' + nsPath + '/) | ' + summary.split('\n')[0] + ' |\n'; + } + + fs.writeFileSync(path.join(OUTPUT_DIR, 'index.md'), indexMd); + console.log('Generated: apidocs/index.md'); + + const types = allItems.filter(i => i.type && ['Class', 'Record', 'Interface', 'Enum', 'Struct'].includes(i.type)); + + for (const [uid, ns] of namespaces) { + const nsPath = ns.name.replace(/\./g, '/'); + const nsDir = path.join(OUTPUT_DIR, nsPath); + fs.mkdirSync(nsDir, { recursive: true }); + + fs.writeFileSync(path.join(nsDir, 'index.md'), generateNamespaceMarkdown(ns, types)); + console.log('Generated: ' + nsPath + '/index.md'); + + const nsTypes = types.filter(t => t.namespace === ns.name); + for (const type of nsTypes) { + const typeName = type.name.split('.').pop(); + const typeDir = path.join(nsDir, typeName); + fs.mkdirSync(typeDir, { recursive: true }); + fs.writeFileSync(path.join(typeDir, 'index.md'), generateTypeMarkdown(type, allItems)); + console.log('Generated: ' + nsPath + '/' + typeName + '/index.md'); + } + } + + console.log('\nAPI documentation generation complete!'); +} + +processYamlFiles(); diff --git a/Website/spec.md b/Website/spec.md new file mode 100644 index 0000000..f3f27e2 --- /dev/null +++ b/Website/spec.md @@ -0,0 +1,5 @@ +- this is primarily about documentaiton and +- Keep the website tight and simple to navigate. +- No duplicate pages +- leverage the readmes for content +- Explain what each element of DataProvider does: LQL, DataProvider, Migrations, etc diff --git a/Website/src/_data/navigation.json b/Website/src/_data/navigation.json new file mode 100644 index 0000000..9675eb9 --- /dev/null +++ b/Website/src/_data/navigation.json @@ -0,0 +1,62 @@ +{ + "main": [ + { "text": "Docs", "url": "/docs/getting-started/" }, + { "text": "API", "url": "/apidocs/" }, + { "text": "Blog", "url": "/blog/" }, + { "text": "GitHub", "url": "https://github.com/MelbourneDeveloper/DataProvider", "external": true } + ], + "docs": [ + { + "title": "Getting Started", + "items": [ + { "text": "Introduction", "url": "/docs/getting-started/" }, + { "text": "Installation", "url": "/docs/installation/" }, + { "text": "Quick Start", "url": "/docs/quick-start/" } + ] + }, + { + "title": "Components", + "items": [ + { "text": "DataProvider", "url": "/docs/dataprovider/" }, + { "text": "LQL", "url": "/docs/lql/" }, + { "text": "Sync", "url": "/docs/sync/" }, + { "text": "Migrations", "url": "/docs/migrations/" }, + { "text": "Gatekeeper", "url": "/docs/gatekeeper/" } + ] + } + ], + "api": [ + { + "title": "API Reference", + "items": [ + { "text": "Overview", "url": "/api/" }, + { "text": "DataProvider API", "url": "/apidocs/" }, + { "text": "LQL Docs", "url": "https://lql.dev", "external": true } + ] + } + ], + "footer": [ + { + "title": "Documentation", + "items": [ + { "text": "Getting Started", "url": "/docs/getting-started/" }, + { "text": "API Reference", "url": "/apidocs/" }, + { "text": "LQL", "url": "/docs/lql/" } + ] + }, + { + "title": "Community", + "items": [ + { "text": "GitHub", "url": "https://github.com/MelbourneDeveloper/DataProvider" }, + { "text": "NuGet", "url": "https://www.nuget.org/packages/DataProvider" } + ] + }, + { + "title": "More", + "items": [ + { "text": "Blog", "url": "/blog/" }, + { "text": "About", "url": "/about/" } + ] + } + ] +} diff --git a/Website/src/_data/site.json b/Website/src/_data/site.json new file mode 100644 index 0000000..e0d9bde --- /dev/null +++ b/Website/src/_data/site.json @@ -0,0 +1,11 @@ +{ + "name": "DataProvider", + "title": "DataProvider - Effortless .NET Data Access", + "description": "Simplifying Database Connectivity in .NET with source-generated SQL, LQL, and offline-first sync.", + "url": "https://dataprovider.dev", + "author": "DataProvider team", + "language": "en", + "themeColor": "#6BCF8E", + "twitter": "@dataprovider", + "github": "https://github.com/MelbourneDeveloper/DataProvider" +} diff --git a/Website/src/_includes/layouts/api.njk b/Website/src/_includes/layouts/api.njk new file mode 100644 index 0000000..ea1b8d6 --- /dev/null +++ b/Website/src/_includes/layouts/api.njk @@ -0,0 +1,43 @@ +--- +layout: layouts/base.njk +--- + +
+ + +
+ {% if namespace %} + + {% endif %} +

{{ title }}

+ + {{ content | safe }} +
+
diff --git a/Website/src/_includes/layouts/base.njk b/Website/src/_includes/layouts/base.njk new file mode 100644 index 0000000..36a55d6 --- /dev/null +++ b/Website/src/_includes/layouts/base.njk @@ -0,0 +1,87 @@ + + + + + + {{ title | default(site.title) }} + + + + + + + + + + + + + + + + + + + + + + {% block head %}{% endblock %} + + + + +
+ +
+ +
+ {% block content %}{{ content | safe }}{% endblock %} +
+ +
+ + + +
+ + + + diff --git a/Website/src/_includes/layouts/blog.njk b/Website/src/_includes/layouts/blog.njk new file mode 100644 index 0000000..fca6174 --- /dev/null +++ b/Website/src/_includes/layouts/blog.njk @@ -0,0 +1,27 @@ +--- +layout: layouts/base.njk +--- + +
+
+

{{ title }}

+

+ + {% if author %} · {{ author }}{% endif %} +

+
+ +
+ {{ content | safe }} + + {% if tags %} +
+ {% for tag in tags %} + {% if tag != 'post' and tag != 'posts' %} + {{ tag }} + {% endif %} + {% endfor %} +
+ {% endif %} +
+
diff --git a/Website/src/_includes/layouts/docs.njk b/Website/src/_includes/layouts/docs.njk new file mode 100644 index 0000000..da3c466 --- /dev/null +++ b/Website/src/_includes/layouts/docs.njk @@ -0,0 +1,41 @@ +--- +layout: layouts/base.njk +--- + +
+ + + +
diff --git a/Website/src/about.md b/Website/src/about.md new file mode 100644 index 0000000..eb4a7e7 --- /dev/null +++ b/Website/src/about.md @@ -0,0 +1,27 @@ +--- +layout: layouts/base.njk +title: About DataProvider +description: About the DataProvider project and team. +--- + +
+
+

About DataProvider

+ +

DataProvider is a comprehensive .NET toolkit for database connectivity, designed to simplify data access while maintaining type safety and performance.

+ +

Our Mission

+

We believe database access in .NET should be simple, type-safe, and performant. DataProvider eliminates boilerplate code while giving developers full control over their SQL.

+ +

Core Principles

+
    +
  • Type Safety First - Source-generated code ensures compile-time checking
  • +
  • No ORM Overhead - Direct SQL execution without mapping layers
  • +
  • Result Types - Error handling without exceptions
  • +
  • Developer Experience - IntelliSense, refactoring, and clean APIs
  • +
+ +

Get Involved

+

DataProvider is open source. Contribute on GitHub or check out our documentation to get started.

+
+
diff --git a/Website/src/api/index.njk b/Website/src/api/index.njk new file mode 100644 index 0000000..e524468 --- /dev/null +++ b/Website/src/api/index.njk @@ -0,0 +1,51 @@ +--- +layout: layouts/base.njk +title: API Reference +description: Auto-generated API documentation for DataProvider. +--- + +
+
+
+

API Reference

+

Auto-generated from XML documentation comments using DocFX.

+
+ + + +
+
+
DP
+

DataProvider

+

Core source generator creating compile-time safe extension methods on IDbConnection and IDbTransaction.

+
+ +
+
SY
+

Sync

+

Offline-first synchronization framework with conflict resolution and real-time subscriptions.

+
+ +
+
MG
+

Migration

+

YAML-based database schema migrations for PostgreSQL, SQLite, and SQL Server.

+
+ +
+
GK
+

Gatekeeper

+

WebAuthn authentication and role-based access control.

+
+
+ + +
+
diff --git a/Website/src/assets/css/styles.css b/Website/src/assets/css/styles.css new file mode 100644 index 0000000..0006416 --- /dev/null +++ b/Website/src/assets/css/styles.css @@ -0,0 +1,636 @@ +/* DataProvider - Design System CSS + Dark-first, high contrast, minimal, reusable classes */ + +:root { + /* Core Colors from Design System */ + --midnight-blue: #0B132B; + --deep-navy: #101A3A; + --slate-blue: #1C2A5A; + --cloud-white: #F4F7FB; + --muted-gray: #9AA4BF; + + /* Accent Colors */ + --data-green: #6BCF8E; + --signal-blue: #4DA3FF; + --query-orange: #FF9F43; + + /* Status Colors */ + --success: #4CAF50; + --warning: #FFB020; + --error: #FF5C5C; + --info: #4DA3FF; + + /* Semantic mappings (dark theme default) */ + --bg-primary: var(--midnight-blue); + --bg-secondary: var(--deep-navy); + --bg-tertiary: var(--slate-blue); + --text-primary: var(--cloud-white); + --text-secondary: var(--muted-gray); + --border-color: var(--slate-blue); + --code-bg: var(--midnight-blue); + + /* Typography */ + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace; + + /* Type Scale */ + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --text-4xl: 2.25rem; + --text-5xl: 3rem; + + /* Spacing */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-12: 3rem; + --space-16: 4rem; + --space-20: 5rem; + + /* Layout */ + --max-width: 1200px; + --header-height: 64px; + --sidebar-width: 260px; + --radius: 8px; + --transition: 200ms ease; +} + +/* Reset */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { + scroll-behavior: smooth; + scroll-padding-top: calc(var(--header-height) + var(--space-4)); + overflow-x: hidden; +} + +body { + font-family: var(--font-sans); + font-size: var(--text-base); + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-primary); + -webkit-font-smoothing: antialiased; + overflow-x: hidden; + width: 100%; + max-width: 100vw; +} + +/* Skip link */ +.skip-link { + position: absolute; + top: -100%; + left: var(--space-4); + padding: var(--space-2) var(--space-4); + background: var(--data-green); + color: var(--midnight-blue); + border-radius: var(--radius); + z-index: 1000; +} +.skip-link:focus { top: var(--space-4); } + +/* Container */ +.container { + width: 100%; + max-width: var(--max-width); + margin: 0 auto; + padding: 0 var(--space-4); +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.25; + color: var(--text-primary); +} +h1 { font-size: var(--text-4xl); font-weight: 700; } +h2 { font-size: var(--text-3xl); } +h3 { font-size: var(--text-2xl); } +h4 { font-size: var(--text-xl); } + +p { margin-bottom: var(--space-4); color: var(--text-secondary); } + +a { color: var(--signal-blue); text-decoration: none; transition: color var(--transition); } +a:hover { color: var(--data-green); } + +ul, ol { margin-bottom: var(--space-4); padding-left: var(--space-6); } +li { margin-bottom: var(--space-2); color: var(--text-secondary); } + +/* Code */ +code { + font-family: var(--font-mono); + font-size: 0.9em; + padding: 0.2em 0.4em; + background: var(--code-bg); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--query-orange); +} + +pre { + font-family: var(--font-mono); + font-size: var(--text-sm); + line-height: 1.7; + padding: var(--space-4); + background: var(--code-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + overflow-x: auto; + margin-bottom: var(--space-4); +} +pre code { padding: 0; background: none; border: none; color: inherit; } + +/* Header */ +.header { + position: sticky; + top: 0; + height: var(--header-height); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + z-index: 100; +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + height: 100%; +} + +.logo { + display: flex; + align-items: center; + gap: var(--space-2); +} +.logo img { height: 32px; } +.logo:hover { opacity: 0.9; } + +.nav-links { + display: flex; + align-items: center; + gap: var(--space-6); + list-style: none; + margin: 0; + padding: 0; +} + +.nav-link { + font-weight: 500; + color: var(--text-secondary); + transition: color var(--transition); +} +.nav-link:hover, .nav-link.active { color: var(--data-green); } + +.nav-actions { display: flex; align-items: center; gap: var(--space-3); } + +/* Mobile menu toggle */ +.mobile-menu-toggle { + display: none; + flex-direction: column; + gap: 4px; + padding: var(--space-2); + background: transparent; + border: none; + cursor: pointer; +} +.mobile-menu-toggle span { + display: block; + width: 24px; + height: 2px; + background: var(--text-primary); + transition: all var(--transition); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-6); + font-family: var(--font-sans); + font-size: var(--text-base); + font-weight: 500; + border-radius: var(--radius); + border: none; + cursor: pointer; + transition: all var(--transition); + text-decoration: none; +} + +.btn-primary { + background: var(--data-green); + color: var(--midnight-blue); +} +.btn-primary:hover { background: #5ab87a; color: var(--midnight-blue); } + +.btn-secondary { + background: transparent; + color: var(--text-primary); + border: 1px solid var(--border-color); +} +.btn-secondary:hover { background: var(--bg-tertiary); } + +.btn-large { padding: var(--space-4) var(--space-8); font-size: var(--text-lg); } + +/* Hero */ +.hero { + position: relative; + padding: var(--space-20) 0; + text-align: center; + background: url('/assets/images/BackgroundImage.png') center/cover no-repeat; +} +.hero::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(11,19,43,0.8) 0%, rgba(11,19,43,0.95) 100%); +} +.hero > * { position: relative; z-index: 1; } +.hero h1 { font-size: var(--text-5xl); margin-bottom: var(--space-6); } +.hero-subtitle { + font-size: var(--text-xl); + color: var(--text-secondary); + max-width: 600px; + margin: 0 auto var(--space-8); +} +.hero-buttons { display: flex; gap: var(--space-4); justify-content: center; flex-wrap: wrap; } +.hero-code { + max-width: 600px; + margin: var(--space-12) auto 0; + text-align: left; +} + +/* Feature cards */ +.features { padding: var(--space-16) 0; } +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: var(--space-6); +} + +.card { + padding: var(--space-6); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + transition: all var(--transition); +} +.card:hover { border-color: var(--data-green); } + +.card-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: var(--data-green); + color: var(--midnight-blue); + border-radius: var(--radius); + margin-bottom: var(--space-4); + font-size: var(--text-xl); + font-weight: 700; +} +.card h3 { margin-bottom: var(--space-2); } +.card p { margin: 0; } +.card a { display: inline-block; margin-top: var(--space-3); } + +/* Section titles */ +.section-title { text-align: center; margin-bottom: var(--space-12); } +.section-title h2 { margin-bottom: var(--space-4); } +.section-title p { max-width: 600px; margin: 0 auto; } + +/* Docs layout */ +.docs-layout { + display: grid; + grid-template-columns: var(--sidebar-width) 1fr; + min-height: calc(100vh - var(--header-height)); +} + +.sidebar { + position: sticky; + top: var(--header-height); + height: calc(100vh - var(--header-height)); + overflow-y: auto; + padding: var(--space-6); + background: var(--bg-secondary); + border-right: 1px solid var(--border-color); +} + +.sidebar-section { margin-bottom: var(--space-6); } +.sidebar-section h4 { + font-size: var(--text-sm); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin-bottom: var(--space-3); +} +.sidebar-section ul { list-style: none; padding: 0; margin: 0; } +.sidebar-section li { margin: 0; } +.sidebar-section a { + display: block; + padding: var(--space-2) var(--space-3); + color: var(--text-secondary); + border-radius: 4px; + transition: all var(--transition); +} +.sidebar-section a:hover, .sidebar-section a.active { + background: var(--bg-tertiary); + color: var(--data-green); +} + +.docs-content { + padding: var(--space-8); + max-width: 900px; +} +.docs-content h1 { margin-bottom: var(--space-6); } +.docs-content h2 { + margin-top: var(--space-10); + margin-bottom: var(--space-4); + padding-bottom: var(--space-2); + border-bottom: 1px solid var(--border-color); +} +.docs-content h3 { margin-top: var(--space-8); margin-bottom: var(--space-3); } + +/* API docs specific */ +.api-header { + padding: var(--space-4); + background: var(--bg-tertiary); + border-radius: var(--radius); + margin-bottom: var(--space-6); +} +.api-header .breadcrumb { + font-size: var(--text-sm); + color: var(--text-secondary); + margin-bottom: var(--space-2); +} +.api-header h1 { margin-bottom: var(--space-2); } +.api-header p { margin: 0; } + +.api-signature { + font-family: var(--font-mono); + padding: var(--space-3); + background: var(--code-bg); + border: 1px solid var(--border-color); + border-radius: 4px; + margin-bottom: var(--space-6); +} + +.api-tabs { + display: flex; + gap: var(--space-1); + border-bottom: 1px solid var(--border-color); + margin-bottom: var(--space-6); +} +.api-tab { + padding: var(--space-3) var(--space-4); + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} +.api-tab:hover, .api-tab.active { + color: var(--data-green); + border-bottom-color: var(--data-green); +} + +/* Blog */ +.blog-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--space-6); +} + +.blog-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + overflow: hidden; + transition: all var(--transition); +} +.blog-card:hover { border-color: var(--data-green); } +.blog-card-image { + height: 180px; + background: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; +} +.blog-card-image img { width: 100%; height: 100%; object-fit: cover; } +.blog-card-content { padding: var(--space-6); } +.blog-card-meta { + font-size: var(--text-sm); + color: var(--text-secondary); + margin-bottom: var(--space-2); +} +.blog-card h3 { margin-bottom: var(--space-2); } +.blog-card h3 a { color: var(--text-primary); } +.blog-card h3 a:hover { color: var(--data-green); } + +.tag { + display: inline-block; + padding: var(--space-1) var(--space-3); + font-size: var(--text-xs); + font-weight: 500; + background: var(--bg-tertiary); + color: var(--text-secondary); + border-radius: 4px; + margin-right: var(--space-1); +} +.tag:hover { background: var(--data-green); color: var(--midnight-blue); } + +/* Search */ +.search-input { + width: 100%; + padding: var(--space-3) var(--space-4); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + color: var(--text-primary); + font-size: var(--text-base); +} +.search-input::placeholder { color: var(--text-secondary); } +.search-input:focus { outline: none; border-color: var(--signal-blue); } + +/* Filter tabs */ +.filter-tabs { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; + margin-bottom: var(--space-8); +} +.filter-tab { + padding: var(--space-2) var(--space-4); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 20px; + color: var(--text-secondary); + font-size: var(--text-sm); + cursor: pointer; + transition: all var(--transition); +} +.filter-tab:hover, .filter-tab.active { + background: var(--data-green); + border-color: var(--data-green); + color: var(--midnight-blue); +} + +/* Footer */ +.footer { + padding: var(--space-16) 0 var(--space-8); + background: var(--bg-secondary); + border-top: 1px solid var(--border-color); +} +.footer-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: var(--space-8); + margin-bottom: var(--space-8); +} +.footer-section h3 { + font-size: var(--text-sm); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: var(--space-4); +} +.footer-section ul { list-style: none; padding: 0; margin: 0; } +.footer-section li { margin-bottom: var(--space-2); } +.footer-section a { color: var(--text-secondary); font-size: var(--text-sm); } +.footer-section a:hover { color: var(--data-green); } + +.footer-bottom { + padding-top: var(--space-8); + border-top: 1px solid var(--border-color); + text-align: center; +} +.footer-bottom p { + font-size: var(--text-sm); + color: var(--text-secondary); + margin-bottom: var(--space-2); +} + +/* Tables */ +table { + width: 100%; + border-collapse: collapse; + margin-bottom: var(--space-6); + background: var(--bg-secondary); + border-radius: var(--radius); + overflow: hidden; +} +th, td { + padding: var(--space-3) var(--space-4); + text-align: left; + border-bottom: 1px solid var(--border-color); +} +th { + background: var(--slate-blue); + color: var(--cloud-white); + font-weight: 600; + text-transform: uppercase; + font-size: var(--text-sm); + letter-spacing: 0.05em; +} +td code { + background: var(--bg-tertiary); + padding: var(--space-1) var(--space-2); + border-radius: 4px; + font-size: var(--text-sm); +} + +/* Breadcrumb */ +.breadcrumb { + font-size: var(--text-sm); + color: var(--text-secondary); + margin-bottom: var(--space-4); +} +.breadcrumb a { color: var(--signal-blue); } + +/* Syntax highlighting */ +.token.comment { color: #6B7280; } +.token.keyword { color: var(--signal-blue); } +.token.string { color: var(--query-orange); } +.token.function, .token.class-name { color: var(--data-green); } +.token.number { color: var(--query-orange); } + +/* Responsive */ +@media (max-width: 1024px) { + .docs-layout { grid-template-columns: 1fr; } + .sidebar { + display: none; + position: fixed; + top: var(--header-height); + left: 0; + width: 100%; + height: calc(100vh - var(--header-height)); + z-index: 50; + } + .sidebar.open { display: block; } +} + +@media (max-width: 768px) { + :root { + --text-5xl: 2.25rem; + --text-4xl: 1.875rem; + --text-3xl: 1.5rem; + } + + .container { padding: 0 var(--space-3); } + + .nav-links { + display: none; + position: fixed; + top: var(--header-height); + left: 0; + width: 100%; + padding: var(--space-4); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + flex-direction: column; + gap: var(--space-2); + } + .nav-links.open { display: flex; } + + .mobile-menu-toggle { display: flex; } + + .nav-actions .btn { display: none; } + + .hero { padding: var(--space-12) 0; } + .hero h1 { font-size: var(--text-4xl); } + .hero-subtitle { font-size: var(--text-base); } + .hero-buttons { flex-direction: column; align-items: center; width: 100%; } + .hero-buttons .btn { width: 100%; max-width: 280px; } + .hero-code { margin: var(--space-8) auto 0; } + + .features-grid, .blog-grid { grid-template-columns: 1fr; } + + .card { padding: var(--space-4); } + + .docs-content { padding: var(--space-4); } + + pre { font-size: var(--text-xs); padding: var(--space-3); } + + table { display: block; overflow-x: auto; } + + .filter-tabs { justify-content: flex-start; } + .filter-tab { font-size: var(--text-xs); padding: var(--space-2) var(--space-3); } + + .footer-grid { gap: var(--space-6); } +} + +/* Utilities */ +.text-center { text-align: center; } +.mt-8 { margin-top: var(--space-8); } +.mb-8 { margin-bottom: var(--space-8); } diff --git a/Website/src/assets/images/AlternativeLogo.png b/Website/src/assets/images/AlternativeLogo.png new file mode 100644 index 0000000..81fece3 Binary files /dev/null and b/Website/src/assets/images/AlternativeLogo.png differ diff --git a/Website/src/assets/images/BackgroundImage.png b/Website/src/assets/images/BackgroundImage.png new file mode 100644 index 0000000..812d7de Binary files /dev/null and b/Website/src/assets/images/BackgroundImage.png differ diff --git a/Website/src/assets/images/Logo.png b/Website/src/assets/images/Logo.png new file mode 100644 index 0000000..608519a Binary files /dev/null and b/Website/src/assets/images/Logo.png differ diff --git a/Website/src/assets/images/LogoAndText.png b/Website/src/assets/images/LogoAndText.png new file mode 100644 index 0000000..dfb18d6 Binary files /dev/null and b/Website/src/assets/images/LogoAndText.png differ diff --git a/Website/src/assets/js/main.js b/Website/src/assets/js/main.js new file mode 100644 index 0000000..604115a --- /dev/null +++ b/Website/src/assets/js/main.js @@ -0,0 +1,81 @@ +(function() { + 'use strict'; + + // Mobile menu toggle + const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); + const navLinks = document.querySelector('.nav-links'); + + if (mobileMenuToggle && navLinks) { + mobileMenuToggle.addEventListener('click', () => { + navLinks.classList.toggle('open'); + mobileMenuToggle.classList.toggle('active'); + }); + + document.addEventListener('click', (e) => { + if (!navLinks.contains(e.target) && !mobileMenuToggle.contains(e.target)) { + navLinks.classList.remove('open'); + mobileMenuToggle.classList.remove('active'); + } + }); + } + + // Docs sidebar toggle (mobile) + const docsSidebar = document.getElementById('docs-sidebar'); + if (docsSidebar) { + const sidebarToggle = document.createElement('button'); + sidebarToggle.className = 'btn btn-primary'; + sidebarToggle.innerHTML = 'Menu'; + sidebarToggle.style.cssText = 'display: none; position: fixed; bottom: 1rem; right: 1rem; z-index: 60;'; + document.body.appendChild(sidebarToggle); + + const checkMobile = () => { + sidebarToggle.style.display = window.innerWidth <= 1024 ? 'block' : 'none'; + }; + checkMobile(); + window.addEventListener('resize', checkMobile); + + sidebarToggle.addEventListener('click', () => { + docsSidebar.classList.toggle('open'); + sidebarToggle.innerHTML = docsSidebar.classList.contains('open') ? 'Close' : 'Menu'; + }); + } + + // Smooth scroll for anchor links + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + const target = document.querySelector(this.getAttribute('href')); + if (target) { + e.preventDefault(); + target.scrollIntoView({ behavior: 'smooth' }); + } + }); + }); + + // Copy button for code blocks + document.querySelectorAll('pre').forEach(pre => { + const wrapper = document.createElement('div'); + wrapper.style.position = 'relative'; + pre.parentNode.insertBefore(wrapper, pre); + wrapper.appendChild(pre); + + const copyBtn = document.createElement('button'); + copyBtn.innerHTML = 'Copy'; + copyBtn.style.cssText = 'position: absolute; top: 0.5rem; right: 0.5rem; padding: 0.25rem 0.75rem; font-size: 0.75rem; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; opacity: 0; transition: opacity 0.2s;'; + + wrapper.appendChild(copyBtn); + wrapper.addEventListener('mouseenter', () => copyBtn.style.opacity = '1'); + wrapper.addEventListener('mouseleave', () => copyBtn.style.opacity = '0'); + + copyBtn.addEventListener('click', async () => { + const code = pre.querySelector('code'); + const text = code ? code.textContent : pre.textContent; + try { + await navigator.clipboard.writeText(text); + copyBtn.innerHTML = 'Copied!'; + setTimeout(() => copyBtn.innerHTML = 'Copy', 2000); + } catch (err) { + copyBtn.innerHTML = 'Failed'; + } + }); + }); +})(); diff --git a/Website/src/blog/connecting-sql-server.md b/Website/src/blog/connecting-sql-server.md new file mode 100644 index 0000000..de5db54 --- /dev/null +++ b/Website/src/blog/connecting-sql-server.md @@ -0,0 +1,46 @@ +--- +layout: layouts/blog.njk +title: Connecting to SQL Server with DataProvider +description: How to connect to SQL Server using the DataProvider toolkit. +date: 2024-04-08 +author: DataProvider Team +tags: + - .NET + - SQL + - Database + - post +--- + +Connecting to SQL Server with DataProvider is straightforward. This guide shows you how to set up your connection and start querying. + +## Setup + +First, install the SQL Server provider: + +```bash +dotnet add package DataProvider.SqlServer +``` + +## Connection + +Create your connection: + +```csharp +using DataProvider.SqlServer; + +var connectionString = "Server=localhost;Database=MyDb;..."; +using var connection = new SqlConnection(connectionString); +var provider = new SqlServerProvider(connection); +``` + +## Querying + +Now you can execute queries: + +```csharp +var customers = provider.Query( + "SELECT * FROM Customers WHERE Active = 1" +); +``` + +Check out the [full documentation](/docs/getting-started/) for more details. diff --git a/Website/src/blog/getting-started-dataprovider.md b/Website/src/blog/getting-started-dataprovider.md new file mode 100644 index 0000000..071713a --- /dev/null +++ b/Website/src/blog/getting-started-dataprovider.md @@ -0,0 +1,42 @@ +--- +layout: layouts/blog.njk +title: Getting Started with the DataProvider Toolkit +description: An introduction to using the DataProvider toolkit in .NET +date: 2024-04-20 +author: DataProvider Team +tags: + - .NET + - C# + - post +--- + +DataProvider is a powerful toolkit for .NET developers that simplifies database connectivity and data access. In this post, we'll walk through the basics of getting started. + +## Installation + +First, add the DataProvider package to your project: + +```bash +dotnet add package DataProvider +``` + +## Your First Query + +With DataProvider installed, you can start executing queries immediately: + +```csharp +using DataProvider; + +var orders = connection.Query( + "SELECT * FROM Orders WHERE Status = @status", + new { status = "Active" } +); +``` + +## Type Safety + +DataProvider generates type-safe extension methods at compile time, giving you IntelliSense support and compile-time checking. + +## Next Steps + +Check out our [documentation](/docs/getting-started/) for more detailed guides and examples. diff --git a/Website/src/blog/index.njk b/Website/src/blog/index.njk new file mode 100644 index 0000000..38e8f55 --- /dev/null +++ b/Website/src/blog/index.njk @@ -0,0 +1,61 @@ +--- +layout: layouts/base.njk +title: Blog +description: Latest news, tutorials, and updates from the DataProvider team. +--- + +
+
+
+

Blogs

+

Latest news, tutorials, and updates from the DataProvider team.

+
+ +
+ +
+ +
+ All Topics + .NET + C# + SQL + LQL + Database +
+ +
+ {% for post in collections.posts %} +
+
+ {% if post.data.image %} + {{ post.data.title }} + {% endif %} +
+
+

+ +

+

{{ post.data.title }}

+

{{ post.data.description }}

+ {% if post.data.tags %} +
+ {% for tag in post.data.tags %} + {% if tag != 'post' and tag != 'posts' %} + {{ tag }} + {% endif %} + {% endfor %} +
+ {% endif %} +
+
+ {% endfor %} +
+ + {% if collections.posts.length == 0 %} +
+

No blog posts yet. Check back soon!

+
+ {% endif %} +
+
diff --git a/Website/src/blog/lql-simplifies-development.md b/Website/src/blog/lql-simplifies-development.md new file mode 100644 index 0000000..69aac84 --- /dev/null +++ b/Website/src/blog/lql-simplifies-development.md @@ -0,0 +1,37 @@ +--- +layout: layouts/blog.njk +title: "LQL: How Lightweight Query Language Simplifies .NET Development" +description: A deep dive into LQL and its benefits for .NET data access. +date: 2024-04-14 +author: DataProvider Team +tags: + - .NET + - LQL + - post +--- + +LQL (Lambda Query Language) is a type-safe query syntax that transpiles to SQL. It allows you to write queries using familiar C# lambda expressions. + +## Why LQL? + +Traditional SQL strings are error-prone and lack type safety. LQL provides: + +- **Compile-time checking**: Catch errors before runtime +- **IntelliSense support**: Full autocomplete in your IDE +- **Refactoring support**: Rename properties safely + +## Basic Example + +```csharp +// LQL +var query = Orders + .Where(o => o.Status == "Active") + .Select(o => new { o.Id, o.Name }); + +// Transpiles to SQL: +// SELECT Id, Name FROM Orders WHERE Status = 'Active' +``` + +## Getting Started + +Try LQL in our [interactive playground](/lql/) or check out the [documentation](/docs/lql/). diff --git a/Website/src/blog/tags.njk b/Website/src/blog/tags.njk new file mode 100644 index 0000000..31dde0d --- /dev/null +++ b/Website/src/blog/tags.njk @@ -0,0 +1,29 @@ +--- +pagination: + data: collections.tagList + size: 1 + alias: tag +permalink: /blog/tags/{{ tag | slugify }}/ +layout: layouts/base.njk +--- + +
+
+

Posts tagged "{{ tag | capitalize }}"

+ + + +

← All posts

+
+
diff --git a/Website/src/docs/dataprovider.md b/Website/src/docs/dataprovider.md new file mode 100644 index 0000000..261cea3 --- /dev/null +++ b/Website/src/docs/dataprovider.md @@ -0,0 +1,214 @@ +--- +layout: layouts/docs.njk +title: "DataProvider SQL Parser" +--- + +# DataProvider SQL Parser + +A .NET source generator project that aims to parse SQL files and generate strongly-typed extension methods for multiple SQL database platforms. + +CRITICAL: The generator connects to the database at compile time to get query metadata. If it doesn't connect, the generation fails with a compiler error. + +**⚠️ Project Status: Early Development** +This project is in active development. Many features described below are partially implemented or planned for future releases. + +## Overview + +- **Input**: `.sql` files and optional `.grouping.json` configuration files +- **Output**: Generated extension methods on database-specific connections +- **Returns**: `Result` instead of throwing exceptions +- **Platforms**: SQL Server, SQLite (with extensible architecture for other databases) + +## Current Implementation Status + +### ✅ What Works +- Basic SQL file processing and code generation +- Result type pattern for error handling +- Database-specific source generators (SQL Server, SQLite) +- Extension method generation for specific connection types +- Basic parameter extraction +- Grouping configuration support via JSON files +- Directory.Build.props with comprehensive Roslyn analyzers + +### ⚠️ Partially Implemented +- **SQL Parsing**: Currently uses SqlParserCS library but falls back to string manipulation for parameter extraction instead of proper AST traversal +- **Code Generation**: Basic structure in place but many areas marked with TODO comments +- **Schema Inspection**: Framework exists but not fully integrated with code generation + +### ❌ Known Issues & Limitations +- **Regex Usage**: The main `DataProviderSourceGenerator` violates project rules by using regex for parameter extraction +- **Extension Target**: Currently generates extensions on `SqlConnection`/`SqliteConnection` rather than `IDbConnection`/`ITransaction` as originally planned +- **JOIN Analysis**: Not currently extracting JOIN information despite SqlStatement structure supporting it +- **SELECT List Extraction**: Not extracting column information from SELECT statements +- **Hardcoded Logic**: Much code generation is specific to example files rather than generic + +## Usage + +1. Add `.sql` files to your project as AdditionalFiles +2. Optionally add corresponding `.grouping.json` files for parent-child relationship mapping +3. Build project → extension methods auto-generated +4. Use generated methods: + +```csharp +// SQLite (currently working) +var result = await sqliteConnection.GetInvoicesAsync(customerName, startDate, endDate); +// Returns: Result, SqlError> with InvoiceLines collection + +// SQL Server (planned) +var result = await sqlConnection.GetInvoicesAsync(customerName, startDate, endDate); +``` + +## Architecture + +### Core Components + +- **SqlFileGeneratorBase**: Base source generator with database-specific implementations +- **SqlStatement**: Generic SQL statement representation (partially populated) +- **ISqlParser**: Abstraction for parsing SQL across different dialects +- **ICodeGenerator**: Abstraction for generating database-specific code +- **Result**: Functional programming style error handling (✅ Complete) + +### Current Generators + +- **DataProvider.SourceGenerator**: Main generator (⚠️ Uses regex - violates project rules) +- **DataProvider.SqlServer**: SQL Server specific generator using SqlParserCS +- **DataProvider.SQLite**: SQLite specific generator using SqlParserCS + +### SQL Parsing Status + +- **Library**: Uses SqlParserCS (✅ Good choice, no regex in parsing) +- **Parameter Extraction**: ⚠️ Falls back to string manipulation instead of AST traversal +- **Query Type Detection**: ✅ Basic implementation +- **JOIN Analysis**: ❌ Infrastructure exists but not populated +- **SELECT List Extraction**: ❌ Not implemented +- **Error Handling**: ✅ Graceful parsing failure with error messages + +## Dependencies + +- **Microsoft.CodeAnalysis** (source generation) +- **SqlParserCS** (SQL parsing across multiple dialects) +- **Microsoft.Data.SqlClient** (SQL Server support) +- **Microsoft.Data.Sqlite** (SQLite support) +- **System.Text.Json** (configuration file parsing) + +## Project Structure + +``` +DataProvider/ # Core types and interfaces +DataProvider.Dependencies/ # Result types and error handling +DataProvider.SourceGenerator/ # ⚠️ Main generator (uses regex) +DataProvider.SqlServer/ # SQL Server source generator +DataProvider.SQLite/ # SQLite source generator +DataProvider.Example/ # Usage examples and test SQL files +DataProvider.Tests/ # Unit tests +DataProvider.Example.Tests/ # Integration tests +``` + +## Configuration Files + +### SQL Files +Standard SQL files with parameterized queries example: +```sql +SELECT i.Id, i.InvoiceNumber, l.Description, l.Amount +FROM Invoice i +JOIN InvoiceLine l ON l.InvoiceId = i.Id +WHERE i.CustomerName = @customerName + AND (@startDate IS NULL OR i.InvoiceDate >= @startDate) +``` + +### Grouping Configuration Example (Optional) +```json +{ + "QueryName": "GetInvoices", + "GroupingStrategy": "ParentChild", + "ParentEntity": { + "Name": "Invoice", + "KeyColumns": ["Id"], + "Columns": ["Id", "InvoiceNumber", "CustomerName"] + }, + "ChildEntity": { + "Name": "InvoiceLine", + "KeyColumns": ["LineId"], + "ParentKeyColumns": ["InvoiceId"], + "Columns": ["LineId", "Description", "Amount"] + } +} +``` + +## Project Rules & Standards + +- **FP Style**: Pure static methods over class methods +- **Result Types**: No exceptions, all operations return `Result` +- **Null Safety**: Comprehensive Roslyn analyzers with strict null checking +- **Code Quality**: All warnings treated as errors, extensive static analysis +- **No Regex**: ⚠️ Currently violated - needs refactoring to use proper AST parsing +- **One Type Per File**: Clean organization with proper namespacing +- **Immutable**: Records over classes, immutable collections + +## Roadmap + +### High Priority Fixes +1. **Remove Regex Usage**: Refactor parameter extraction to use SqlParserCS AST properly +2. **Complete SQL Parsing**: Extract SELECT lists, tables, and JOIN information +3. **Fix Extension Targets**: Generate extensions on `IDbConnection`/`ITransaction` interfaces +4. **Generic Code Generation**: Remove hardcoded logic for specific examples + +### Future Enhancements +1. **Schema Integration**: Use database schema inspection for type generation +2. **Multiple Result Sets**: Support for stored procedures with multiple result sets +3. **Query Optimization**: Analysis and suggestions for query performance +4. **Additional Databases**: PostgreSQL, MySQL support + +## Example + +Given a SQL file `GetInvoices.sql`: + +```sql +SELECT + i.Id, + i.InvoiceNumber, + i.CustomerName, + l.Description, + l.Amount +FROM Invoice i +JOIN InvoiceLine l ON l.InvoiceId = i.Id +WHERE i.CustomerName = @customerName +``` + +The generator currently creates code like this. These are only examples. Don't put invoice specific code in the code generator: + +```csharp +// ⚠️ Current implementation - needs improvement +public static async Task, SqlError>> GetInvoicesAsync( + this SqliteConnection connection, + string customerName) +{ + // Generated implementation with basic error handling + // TODO: Improve type mapping and parameter handling +} + +public record Invoice( + int Id, + string InvoiceNumber, + string CustomerName, + ImmutableList InvoiceLines +); + +public record InvoiceLine( + string Description, + decimal Amount +); +``` + +## Contributing + +This project follows strict coding standards enforced by Roslyn analyzers. Key principles: + +- All warnings treated as errors +- Comprehensive null safety analysis +- Functional programming patterns preferred +- Result types instead of exceptions +- No regex - use proper parsing libraries +- Extensive XML documentation required + +See `Directory.Build.props` and `CodeAnalysis.ruleset` for complete rules. diff --git a/Website/src/docs/gatekeeper.md b/Website/src/docs/gatekeeper.md new file mode 100644 index 0000000..5f26314 --- /dev/null +++ b/Website/src/docs/gatekeeper.md @@ -0,0 +1,41 @@ +--- +layout: layouts/docs.njk +title: Gatekeeper +description: WebAuthn authentication and RBAC for your APIs. +--- + +## Overview + +Gatekeeper provides WebAuthn-based authentication and Role-Based Access Control (RBAC) for your DataProvider APIs. + +## Features + +- **WebAuthn**: Passwordless authentication using FIDO2 +- **RBAC**: Role-based access control +- **API Security**: Protect your data endpoints +- **Integration**: Works seamlessly with DataProvider + +## Setup + +```csharp +services.AddGatekeeper(options => +{ + options.RelyingPartyId = "your-domain.com"; + options.RelyingPartyName = "Your App"; +}); +``` + +## Protecting Endpoints + +```csharp +[Authorize(Roles = "Admin")] +public async Task GetSecureData() +{ + // Protected endpoint +} +``` + +## Next Steps + +- [DataProvider Documentation](/docs/dataprovider/) +- [Sync Documentation](/docs/sync/) diff --git a/Website/src/docs/getting-started.md b/Website/src/docs/getting-started.md new file mode 100644 index 0000000..5525af4 --- /dev/null +++ b/Website/src/docs/getting-started.md @@ -0,0 +1,43 @@ +--- +layout: layouts/docs.njk +title: Getting Started +description: Get started with DataProvider for .NET data access. +--- + +## Installation + +Add DataProvider to your .NET project: + +```bash +dotnet add package DataProvider +``` + +## Quick Start + +DataProvider generates type-safe extension methods on `IDbConnection` from your SQL queries. + +```csharp +using DataProvider; + +// Execute a query +var orders = connection.Query("SELECT * FROM Orders WHERE Status = @status", + new { status = "Active" }); + +// Insert a record +connection.Execute("INSERT INTO Orders (Name, Status) VALUES (@name, @status)", + new { name = "New Order", status = "Pending" }); +``` + +## Core Concepts + +DataProvider is built around these key principles: + +- **Source Generation**: SQL queries generate type-safe extension methods at compile time +- **No ORM Overhead**: Direct SQL execution without mapping layers +- **Result Types**: All operations return `Result` for error handling without exceptions + +## Next Steps + +- [DataProvider Documentation](/docs/dataprovider/) +- [LQL Query Language](/docs/lql/) +- [API Reference](/api/) diff --git a/Website/src/docs/index.md b/Website/src/docs/index.md new file mode 100644 index 0000000..b594fd3 --- /dev/null +++ b/Website/src/docs/index.md @@ -0,0 +1,25 @@ +--- +layout: layouts/docs.njk +title: Documentation +description: DataProvider documentation and guides. +--- + +Welcome to the DataProvider documentation. Get started with our comprehensive guides. + +## Quick Links + +- [Getting Started](/docs/getting-started/) - Introduction and setup +- [Installation](/docs/installation/) - Install DataProvider packages +- [Quick Start](/docs/quick-start/) - Start coding immediately + +## Components + +- [DataProvider](/docs/dataprovider/) - Source-generated SQL extensions +- [LQL](/docs/lql/) - Lambda Query Language +- [Sync](/docs/sync/) - Offline-first synchronization +- [Migrations](/docs/migrations/) - YAML-based migrations +- [Gatekeeper](/docs/gatekeeper/) - WebAuthn & RBAC + +## API Reference + +Browse the complete [API Reference](/api/) for detailed class and method documentation. diff --git a/Website/src/docs/installation.md b/Website/src/docs/installation.md new file mode 100644 index 0000000..273c2b0 --- /dev/null +++ b/Website/src/docs/installation.md @@ -0,0 +1,64 @@ +--- +layout: layouts/docs.njk +title: Installation +description: How to install DataProvider in your .NET project. +--- + +## NuGet Package + +Install DataProvider via NuGet: + +```bash +dotnet add package DataProvider +``` + +Or using the Package Manager Console: + +```powershell +Install-Package DataProvider +``` + +## Database Providers + +Install the provider for your database: + +### SQL Server + +```bash +dotnet add package DataProvider.SqlServer +``` + +### MySQL + +```bash +dotnet add package DataProvider.MySql +``` + +### SQLite + +```bash +dotnet add package DataProvider.Sqlite +``` + +## Requirements + +- .NET 9.0 or later +- C# 13 or later +- Nullable reference types enabled + +## Project Configuration + +Add the following to your `.csproj`: + +```xml + + net9.0 + enable + latest + +``` + +## Next Steps + +- [Quick Start Guide](/docs/quick-start/) +- [DataProvider Documentation](/docs/dataprovider/) diff --git a/Website/src/docs/lql.md b/Website/src/docs/lql.md new file mode 100644 index 0000000..bd0e107 --- /dev/null +++ b/Website/src/docs/lql.md @@ -0,0 +1,243 @@ +--- +layout: layouts/docs.njk +title: "Lambda Query Language (LQL)" +--- + +# Lambda Query Language (LQL) + +A functional pipeline-style DSL that transpiles to SQL. LQL provides an intuitive, composable way to write database queries using lambda expressions and pipeline operators, making complex queries more readable and maintainable. + +## Website + +Visit [lql.dev](https://lql.dev) for interactive playground and documentation. + +## Features + +- **Pipeline Syntax** - Chain operations using `|>` operator +- **Lambda Expressions** - Use familiar lambda syntax for filtering +- **Cross-Database Support** - Transpiles to PostgreSQL, SQLite, and SQL Server +- **Type Safety** - Integrates with DataProvider for compile-time validation +- **VS Code Extension** - Syntax highlighting and IntelliSense support +- **CLI Tools** - Command-line transpilation and validation + +## Syntax Overview + +### Basic Pipeline +```lql +users |> select(id, name, email) +``` + +### With Filtering +```lql +employees +|> filter(fn(row) => row.salary > 50000) +|> select(id, name, salary) +``` + +### Joins +```lql +Customer +|> join(Order, on = Customer.Id = Order.CustomerId) +|> select(Customer.Name, Order.Total) +``` + +### Complex Queries +```lql +let high_value_customers = Customer +|> join(Order, on = Customer.Id = Order.CustomerId) +|> filter(fn(row) => row.Order.Total > 1000) +|> group_by(Customer.Id, Customer.Name) +|> having(fn(row) => SUM(row.Order.Total) > 5000) +|> select(Customer.Name, SUM(Order.Total) AS TotalSpent) +|> order_by(TotalSpent DESC) +|> limit(10) +``` + +## Pipeline Operations + +| Operation | Description | SQL Equivalent | +|-----------|-------------|----------------| +| `select(cols...)` | Choose columns | `SELECT` | +| `filter(fn(row) => ...)` | Filter rows | `WHERE` | +| `join(table, on = ...)` | Join tables | `JOIN` | +| `left_join(table, on = ...)` | Left join | `LEFT JOIN` | +| `group_by(cols...)` | Group rows | `GROUP BY` | +| `having(fn(row) => ...)` | Filter groups | `HAVING` | +| `order_by(col [ASC/DESC])` | Sort results | `ORDER BY` | +| `limit(n)` | Limit rows | `LIMIT` | +| `offset(n)` | Skip rows | `OFFSET` | +| `distinct()` | Unique rows | `DISTINCT` | +| `union(query)` | Combine queries | `UNION` | +| `union_all(query)` | Combine with duplicates | `UNION ALL` | + +## Installation + +### CLI Tool (SQLite) +```bash +dotnet tool install -g LqlCli.SQLite +``` + +### VS Code Extension +Search for "LQL" in VS Code Extensions or: +```bash +code --install-extension lql-lang +``` + +### NuGet Packages +```xml + + + + + + + + +``` + +## CLI Usage + +### Transpile to SQL +```bash +lql --input query.lql --output query.sql +``` + +### Validate Syntax +```bash +lql --input query.lql --validate +``` + +### Print to Console +```bash +lql --input query.lql +``` + +## Programmatic Usage + +```csharp +using Lql; +using Lql.SQLite; + +// Parse LQL +var lqlCode = "users |> filter(fn(row) => row.age > 21) |> select(name, email)"; +var statement = LqlCodeParser.Parse(lqlCode); + +// Convert to SQL +var context = new SQLiteContext(); +var sql = statement.ToSql(context); + +Console.WriteLine(sql); +// Output: SELECT name, email FROM users WHERE age > 21 +``` + +## Function Support + +### Aggregate Functions +- `COUNT()`, `SUM()`, `AVG()`, `MIN()`, `MAX()` + +### String Functions +- `UPPER()`, `LOWER()`, `LENGTH()`, `CONCAT()` + +### Date Functions +- `NOW()`, `DATE()`, `YEAR()`, `MONTH()` + +### Conditional +- `CASE WHEN ... THEN ... ELSE ... END` +- `COALESCE()`, `NULLIF()` + +## Expression Support + +### Arithmetic +```lql +products |> select(price * quantity AS total) +``` + +### Comparisons +```lql +orders |> filter(fn(row) => row.date >= '2024-01-01' AND row.status != 'cancelled') +``` + +### Pattern Matching +```lql +customers |> filter(fn(row) => row.name LIKE 'John%') +``` + +### Subqueries +```lql +orders |> filter(fn(row) => row.customer_id IN ( + customers |> filter(fn(c) => c.country = 'USA') |> select(id) +)) +``` + +## VS Code Extension Features + +- Syntax highlighting +- Auto-completion +- Error diagnostics +- Format on save +- Snippets for common patterns + +## Architecture + +``` +Lql/ +├── Lql/ # Core transpiler +│ ├── Parsing/ # ANTLR grammar and parser +│ ├── FunctionMapping/ # Database-specific functions +│ └── Pipeline steps # AST transformation +├── Lql.SQLite/ # SQLite dialect +├── Lql.SqlServer/ # SQL Server dialect +├── Lql.Postgres/ # PostgreSQL dialect +├── LqlCli.SQLite/ # CLI tool +├── LqlExtension/ # VS Code extension +└── Website/ # lql.dev website +``` + +## Testing + +```bash +dotnet test Lql.Tests/Lql.Tests.csproj +``` + +## Examples + +See the `Lql.Tests/TestData/Lql/` directory for comprehensive examples of LQL queries and their SQL equivalents. + +## Error Handling + +LQL provides detailed error messages: + +```lql +// Invalid: Identifier cannot start with number +123table |> select(id) +// Error: Syntax error at line 1:0 - Identifier cannot start with a number + +// Invalid: Undefined variable +undefined_var |> select(name) +// Error: Syntax error at line 1:0 - Undefined variable +``` + +## Integration with DataProvider + +LQL files are automatically processed by DataProvider source generators: + +1. Write `.lql` files in your project +2. DataProvider transpiles to SQL during build +3. Generates type-safe C# extension methods +4. Use with full IntelliSense support + +## Contributing + +1. Follow functional programming principles +2. Add tests for new features +3. Update grammar file for syntax changes +4. Ensure all dialects are supported +5. Run tests before submitting PRs + +## License + +MIT License + +## Author + +MelbourneDeveloper - [ChristianFindlay.com](https://christianfindlay.com) diff --git a/Website/src/docs/quick-start.md b/Website/src/docs/quick-start.md new file mode 100644 index 0000000..4c93203 --- /dev/null +++ b/Website/src/docs/quick-start.md @@ -0,0 +1,82 @@ +--- +layout: layouts/docs.njk +title: Quick Start +description: Get up and running with DataProvider in minutes. +--- + +## Create a Connection + +```csharp +using System.Data.SqlClient; +using DataProvider; + +var connectionString = "Server=localhost;Database=MyDb;..."; +using var connection = new SqlConnection(connectionString); +connection.Open(); +``` + +## Execute Queries + +### Select + +```csharp +var orders = connection.Query( + "SELECT * FROM Orders WHERE Status = @status", + new { status = "Active" } +); + +foreach (var order in orders) +{ + Console.WriteLine($"{order.Id}: {order.Name}"); +} +``` + +### Insert + +```csharp +var affectedRows = connection.Execute( + "INSERT INTO Orders (Name, Status) VALUES (@name, @status)", + new { name = "New Order", status = "Pending" } +); +``` + +### Update + +```csharp +connection.Execute( + "UPDATE Orders SET Status = @status WHERE Id = @id", + new { status = "Completed", id = 123 } +); +``` + +### Delete + +```csharp +connection.Execute( + "DELETE FROM Orders WHERE Id = @id", + new { id = 123 } +); +``` + +## Using Transactions + +```csharp +using var transaction = connection.BeginTransaction(); +try +{ + transaction.Execute("INSERT INTO ..."); + transaction.Execute("UPDATE ..."); + transaction.Commit(); +} +catch +{ + transaction.Rollback(); + throw; +} +``` + +## Next Steps + +- [DataProvider Documentation](/docs/dataprovider/) +- [LQL Query Language](/docs/lql/) +- [API Reference](/api/) diff --git a/Website/src/docs/sync.md b/Website/src/docs/sync.md new file mode 100644 index 0000000..f541ae9 --- /dev/null +++ b/Website/src/docs/sync.md @@ -0,0 +1,409 @@ +--- +layout: layouts/docs.njk +title: "Sync Framework" +--- + +# Sync Framework + +A database-agnostic, offline-first synchronization framework for .NET applications. Enables two-way data synchronization between distributed replicas with conflict resolution, tombstone management, and real-time subscriptions. + +## Overview + +The Sync framework provides: + +- **Offline-first architecture** - Work locally, sync when connected +- **Two-way synchronization** - Pull changes from server, push local changes +- **Conflict resolution** - Last-write-wins, server-wins, client-wins, or custom strategies +- **Foreign key handling** - Automatic deferred retry for FK violations during sync +- **Tombstone management** - Safe deletion tracking for late-syncing clients +- **Real-time subscriptions** - Subscribe to changes on specific records or tables +- **Hash verification** - SHA-256 integrity checking for batches and databases +- **Database agnostic** - Currently supports SQLite and PostgreSQL + +## Projects + +| Project | Description | +|---------|-------------| +| `Sync` | Core synchronization engine (platform-agnostic) | +| `Sync.SQLite` | SQLite-specific implementation | +| `Sync.Postgres` | PostgreSQL-specific implementation | +| `Sync.Api` | REST API server with SSE real-time subscriptions | +| `Sync.Tests` | Core engine tests | +| `Sync.SQLite.Tests` | SQLite integration tests | +| `Sync.Postgres.Tests` | PostgreSQL integration tests | +| `Sync.Api.Tests` | API endpoint tests | +| `Sync.Integration.Tests` | Cross-database E2E tests | + +## Getting Started + +### Prerequisites + +- .NET 9.0 SDK +- For PostgreSQL: Docker (or a local PostgreSQL instance) + +### Installation + +Add the appropriate NuGet packages to your project: + +```xml + + + + + + + +``` + +### Basic Setup (SQLite) + +#### 1. Initialize the Sync Schema + +```csharp +using Microsoft.Data.Sqlite; +using Sync.SQLite; + +// Create your database connection +using var connection = new SqliteConnection("Data Source=myapp.db"); +connection.Open(); + +// Create sync tables (_sync_log, _sync_state, _sync_session, etc.) +SyncSchema.CreateSchema(connection); +SyncSchema.InitializeSyncState(connection, originId: Guid.NewGuid().ToString()); +``` + +#### 2. Add Triggers to Your Tables + +```csharp +// Generate and apply sync triggers for a table +var triggerResult = TriggerGenerator.GenerateTriggers(connection, "Person"); +if (triggerResult is TriggerListOk ok) +{ + foreach (var trigger in ok.Value) + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = trigger; + cmd.ExecuteNonQuery(); + } +} +``` + +This creates INSERT, UPDATE, and DELETE triggers that automatically log changes to `_sync_log`. + +#### 3. Perform Synchronization + +```csharp +using Sync; + +// Create delegate functions for database operations +Func fetchRemoteChanges = (fromVersion, batchSize) => + SyncLogRepository.FetchChanges(remoteConnection, fromVersion, batchSize); + +Func applyChange = (entry) => + ChangeApplierSQLite.ApplyChange(localConnection, entry); + +Func enableSuppression = () => + SyncSessionManager.EnableSuppression(localConnection); + +Func disableSuppression = () => + SyncSessionManager.DisableSuppression(localConnection); + +// Pull changes from remote +var pullResult = SyncCoordinator.Pull( + fetchRemoteChanges, + applyChange, + enableSuppression, + disableSuppression, + getLastServerVersion: () => SyncLogRepository.GetLastServerVersion(localConnection), + updateLastServerVersion: (v) => SyncLogRepository.UpdateLastServerVersion(localConnection, v), + localOriginId: myOriginId, + config: new BatchConfig(BatchSize: 1000, MaxRetryPasses: 3), + logger: NullLogger.Instance +); + +// Push local changes to remote +var pushResult = SyncCoordinator.Push( + fetchLocalChanges: (fromVersion, batchSize) => + SyncLogRepository.FetchChanges(localConnection, fromVersion, batchSize), + sendToRemote: (batch) => ApplyBatchToRemote(remoteConnection, batch), + getLastPushVersion: () => SyncLogRepository.GetLastPushVersion(localConnection), + updateLastPushVersion: (v) => SyncLogRepository.UpdateLastPushVersion(localConnection, v), + config: new BatchConfig(), + logger: NullLogger.Instance +); +``` + +### Using the REST API + +#### Start the API Server + +```bash +cd Sync/Sync.Api +dotnet run +``` + +#### API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Health check | +| `/sync/changes` | GET | Pull changes from server | +| `/sync/changes` | POST | Push changes to server | +| `/sync/clients` | POST | Register a sync client | +| `/sync/state` | GET | Get server sync state | +| `/sync/subscribe` | GET | Subscribe to real-time changes (SSE) | +| `/sync/subscribe/{id}` | DELETE | Unsubscribe | + +#### Pull Changes + +```bash +curl "http://localhost:5000/sync/changes?fromVersion=0&batchSize=100&connectionString=Data%20Source=server.db&dbType=sqlite" +``` + +Response: +```json +{ + "changes": [ + { + "version": 1, + "tableName": "Person", + "pkValue": "{\"Id\":1}", + "operation": "Insert", + "payload": "{\"Id\":1,\"Name\":\"Alice\",\"Email\":\"alice@example.com\"}", + "origin": "client-abc", + "timestamp": "2025-01-15T10:30:00.000Z" + } + ], + "fromVersion": 0, + "toVersion": 1, + "hasMore": false +} +``` + +#### Push Changes + +```bash +curl -X POST "http://localhost:5000/sync/changes?connectionString=Data%20Source=server.db&dbType=sqlite" \ + -H "Content-Type: application/json" \ + -d '{ + "OriginId": "client-xyz", + "Changes": [ + { + "version": 0, + "tableName": "Person", + "pkValue": "{\"Id\":2}", + "operation": "Insert", + "payload": "{\"Id\":2,\"Name\":\"Bob\"}", + "origin": "client-xyz", + "timestamp": "2025-01-15T11:00:00.000Z" + } + ] + }' +``` + +#### Real-Time Subscriptions (SSE) + +```bash +# Subscribe to all changes on the Person table +curl "http://localhost:5000/sync/subscribe?tableName=Person" + +# Subscribe to a specific record +curl "http://localhost:5000/sync/subscribe?tableName=Person&pkValue=1" +``` + +### PostgreSQL Setup + +#### 1. Start PostgreSQL with Docker + +From the repository root: + +```bash +docker-compose up -d +``` + +This starts a single PostgreSQL container on `localhost:5432` (user: postgres, password: postgres, database: gigs). The C# migrations handle schema creation. + +#### 2. Initialize Schema + +```csharp +using Npgsql; +using Sync.Postgres; + +using var connection = new NpgsqlConnection( + "Host=localhost;Port=5432;Database=gigs;Username=postgres;Password=postgres"); +connection.Open(); + +PostgresSyncSchema.CreateSchema(connection); +PostgresSyncSchema.InitializeSyncState(connection, originId: Guid.NewGuid().ToString()); +``` + +## Architecture + +### Sync Tables + +The framework creates these tables in your database: + +| Table | Purpose | +|-------|---------| +| `_sync_log` | Change log with version, table, PK, operation, payload, origin, timestamp | +| `_sync_state` | Local replica state (origin_id, last_server_version, last_push_version) | +| `_sync_session` | Trigger suppression flag (sync_active) | +| `_sync_clients` | Server-side client tracking for tombstone management | +| `_sync_subscriptions` | Real-time subscription registrations | + +### Change Capture + +When you modify a tracked table: +1. AFTER trigger fires (if `sync_active = 0`) +2. Trigger inserts row into `_sync_log` with: + - Auto-incrementing version + - Table name and primary key (JSON) + - Operation (Insert/Update/Delete) + - Full row payload (JSON) for Insert/Update, NULL for Delete + - Origin ID (prevents echo during sync) + - UTC timestamp + +### Sync Flow + +**Pull (receive changes):** +1. Enable trigger suppression (`sync_active = 1`) +2. Fetch batch from remote (version > lastServerVersion) +3. Apply changes with FK violation defer/retry +4. Skip changes from own origin (echo prevention) +5. Update lastServerVersion +6. Repeat until no more changes +7. Disable trigger suppression + +**Push (send changes):** +1. Fetch local changes (version > lastPushVersion) +2. Send batch to remote +3. Update lastPushVersion +4. Repeat until no more changes + +### Conflict Resolution + +When the same row is modified by different origins: + +```csharp +// Default: Last-write-wins (by timestamp, version as tiebreaker) +var resolved = ConflictResolver.Resolve( + localEntry, + remoteEntry, + ConflictStrategy.LastWriteWins +); + +// Or use custom resolution +var resolved = ConflictResolver.ResolveCustom( + localEntry, + remoteEntry, + (local, remote) => /* your merge logic */ +); +``` + +### Hash Verification + +Verify data integrity with SHA-256 hashes: + +```csharp +// Hash a batch of changes +var batchHash = HashVerifier.ComputeBatchHash(changes); + +// Hash entire database state +var dbHash = HashVerifier.ComputeDatabaseHash( + fetchAllChanges: () => SyncLogRepository.FetchAll(connection) +); + +// Verify batch integrity +var isValid = HashVerifier.VerifyHash(expectedHash, actualHash); +``` + +## Running Tests + +```bash +# All tests +dotnet test + +# Specific test projects +dotnet test --filter "FullyQualifiedName~Sync.Tests" +dotnet test --filter "FullyQualifiedName~Sync.SQLite.Tests" +dotnet test --filter "FullyQualifiedName~Sync.Postgres.Tests" +dotnet test --filter "FullyQualifiedName~Sync.Api.Tests" + +# Cross-database integration tests (requires Docker) +dotnet test --filter "FullyQualifiedName~Sync.Integration.Tests" +``` + +## Configuration + +### BatchConfig + +```csharp +var config = new BatchConfig( + BatchSize: 1000, // Changes per batch (default: 1000) + MaxRetryPasses: 3 // FK violation retry attempts (default: 3) +); +``` + +### Tombstone Management + +```csharp +// Calculate safe version to purge (all clients have synced past this) +var safeVersion = TombstoneManager.CalculateSafePurgeVersion( + getAllClients: () => SyncClientRepository.GetAll(connection) +); + +// Purge old tombstones +TombstoneManager.PurgeTombstones( + purge: (version) => SyncLogRepository.PurgeBefore(connection, version), + safeVersion +); + +// Detect stale clients (90 days inactive by default) +var staleClients = TombstoneManager.FindStaleClients( + getAllClients: () => SyncClientRepository.GetAll(connection), + inactivityThreshold: TimeSpan.FromDays(90) +); +``` + +## Error Handling + +All operations return `Result`: + +```csharp +var result = SyncCoordinator.Pull(...); + +if (result is PullResultOk ok) +{ + Console.WriteLine($"Pulled {ok.Value.ChangesApplied} changes"); +} +else if (result is PullResultError error) +{ + switch (error.Value) + { + case SyncErrorForeignKeyViolation fk: + Console.WriteLine($"FK violation: {fk.Message}"); + break; + case SyncErrorFullResyncRequired: + Console.WriteLine("Client fell too far behind, full resync needed"); + break; + case SyncErrorHashMismatch hash: + Console.WriteLine($"Data integrity error: {hash.Expected} != {hash.Actual}"); + break; + // ... handle other error types + } +} +``` + +## Design Principles + +This framework follows the coding rules from `CLAUDE.md`: + +- **No exceptions** - All fallible operations return `Result` +- **No classes** - Uses records and static methods (FP style) +- **No interfaces** - Uses `Func` and `Action` for abstractions +- **Integration testing** - No mocks, tests use real databases +- **Copious logging** - All operations log via `ILogger` + +## License + +See repository root for license information. diff --git a/Website/src/index.njk b/Website/src/index.njk new file mode 100644 index 0000000..eb03e7a --- /dev/null +++ b/Website/src/index.njk @@ -0,0 +1,127 @@ +--- +layout: layouts/base.njk +title: "DataProvider - Effortless .NET Data Access" +description: "Simplifying Database Connectivity in .NET with source-generated SQL, LQL, and offline-first sync." +--- + +
+
+

Effortless .NET Data Access

+

+ Simplifying Database Connectivity in .NET +

+ + +
+{% highlight "sql" %} +SELECT * FROM Orders +WHERE Status = 'Active'; +{% endhighlight %} +
+
+
+ +
+
+
+

The DataProvider Stack

+

A complete toolkit for .NET database access.

+
+ +
+
+
LQL
+

LQL Integration

+

Seamlessly integrated with LQL for intuitive queries. Write type-safe Lambda Query Language that compiles to SQL.

+ Learn more → +
+ +
+
DB
+

Multiple Databases

+

Connect to SQL Server, MySQL, SQLite, and more with a unified API and connection providers.

+ Learn more → +
+ +
+
HI
+

High Performance

+

Optimized for fast and efficient data access with source-generated extension methods.

+ API Reference → +
+
+
+
+ +
+
+
+

Core Components

+

Everything you need for modern .NET data access.

+
+ +
+
+
DP
+

DataProvider

+

Source generator for SQL that creates type-safe extension methods on IDbConnection.

+ Documentation → +
+ +
+
LQ
+

LQL

+

Lambda Query Language - a type-safe query syntax that transpiles to SQL.

+ Documentation → +
+ +
+
SY
+

Sync

+

Offline-first bidirectional synchronization for mobile and web applications.

+ Documentation → +
+ +
+
GK
+

Gatekeeper

+

WebAuthn authentication and Role-Based Access Control for your APIs.

+ Documentation → +
+ +
+
MI
+

Migrations

+

YAML-based database migrations with the Migration CLI tool.

+ Documentation → +
+
+
+
+ +
+
+
+

Get Started in Minutes

+
+ +
+{% highlight "bash" %} +# Install the DataProvider package +dotnet add package DataProvider + +# Create your first query +using DataProvider; + +var orders = connection.Query("SELECT * FROM Orders"); +{% endhighlight %} +
+ + +
+
diff --git a/Website/src/robots.txt b/Website/src/robots.txt new file mode 100644 index 0000000..95acb72 --- /dev/null +++ b/Website/src/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://dataprovider.dev/sitemap.xml diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f8984f2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "DataProviderWebsite", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "js-yaml": "^4.1.1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..53d87a2 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "js-yaml": "^4.1.1" + } +}