This commit is contained in:
2025-12-26 16:40:32 +03:00
commit 4f1be2c3db
37 changed files with 2222 additions and 0 deletions

484
.gitignore vendored Normal file
View File

@@ -0,0 +1,484 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from `dotnet new gitignore`
# dotenv files
.env
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea/
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Vim temporary swap files
*.swp

View File

@@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Mvc;
using public_valetax.DTOs;
using public_valetax.Exceptions;
using public_valetax.Repositories;
namespace public_valetax.Controllers
{
[ApiController]
[Route("api.[controller]")]
public class JournalController(IJournalRepository _journalRepository) : ControllerBase
{
[HttpPost("getRange")]
public async Task<ActionResult<MRange_MJournalInfo>> GetRange([FromQuery] int skip, [FromQuery] int take, [FromBody] VJournalFilter filter)
{
var result = await _journalRepository.GetJournalEntriesRangeAsync(skip, take, filter);
return Ok(result);
}
[HttpPost("getSingle")]
public async Task<ActionResult<MJournal?>> GetSingle([FromQuery] long id)
{
var result = await _journalRepository.GetJournalEntryAsync(id);
if (result == null)
{
return NotFound();
}
return Ok(result);
}
[HttpPost("simulateError")]
public void SimulateError([FromQuery] bool isSecureException, [FromBody] Dictionary<string, object> someBody)
{
if (isSecureException)
{
throw new SecureException("Some secure error happened");
}
else
{
throw new InvalidOperationException("Some error happened");
}
}
}
}

View File

@@ -0,0 +1,83 @@
using Microsoft.AspNetCore.Mvc;
using public_valetax.Repositories;
using public_valetax.Exceptions;
namespace public_valetax.Controllers
{
[ApiController]
[Route("api.[controller]")]
public class NodeController(ITreeRepository _treeRepository) : ControllerBase
{
[HttpPost("create")]
public async Task<IActionResult> Create(
[FromQuery] string treeName,
[FromQuery] long? parentNodeId,
[FromQuery] string nodeName)
{
try
{
// Get or create the tree
var tree = await _treeRepository.GetTreeByNameAsync(treeName);
if (tree == null)
{
tree = await _treeRepository.CreateTreeAsync(treeName);
}
// Create the node
var node = await _treeRepository.CreateNodeAsync(tree.Id, parentNodeId, nodeName);
return Ok();
}
catch (SecureException)
{
// Re-throw secure exceptions to be handled by middleware
throw;
}
catch (Exception ex)
{
// Wrap other exceptions in SecureException to trigger proper error handling
throw new SecureException(ex.Message, ex);
}
}
[HttpPost("delete")]
public async Task<IActionResult> Delete([FromQuery] long nodeId)
{
try
{
await _treeRepository.DeleteNodeAsync(nodeId);
return Ok();
}
catch (SecureException)
{
// Re-throw secure exceptions to be handled by middleware
throw;
}
catch (Exception ex)
{
// Wrap other exceptions in SecureException to trigger proper error handling
throw new SecureException(ex.Message, ex);
}
}
[HttpPost("rename")]
public async Task<IActionResult> Rename([FromQuery] long nodeId, [FromQuery] string newNodeName)
{
try
{
await _treeRepository.RenameNodeAsync(nodeId, newNodeName);
return Ok();
}
catch (SecureException)
{
// Re-throw secure exceptions to be handled by middleware
throw;
}
catch (Exception ex)
{
// Wrap other exceptions in SecureException to trigger proper error handling
throw new SecureException(ex.Message, ex);
}
}
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Mvc;
using public_valetax.DTOs;
namespace public_valetax.Controllers
{
[ApiController]
[Route("api.[controller]")]
public class PartnerController : ControllerBase
{
[HttpPost("rememberMe")]
public ActionResult<TokenInfo> RememberMe([FromQuery] string code)
{
// This is a simplified implementation
// In a real application, you would validate the code and generate a proper JWT token
var token = $"generated_token_for_code_{code}";
return Ok(new TokenInfo { Token = token });
}
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using public_valetax.DTOs;
using public_valetax.Repositories;
namespace public_valetax.Controllers
{
[ApiController]
[Route("api.[controller]")]
public class TreeController(ITreeRepository _treeRepository) : ControllerBase
{
[HttpPost("get")]
public async Task<ActionResult<MNode>> Get([FromQuery] string treeName)
{
var tree = await _treeRepository.GetTreeStructureAsync(treeName);
// If tree doesn't exist, create it
if (tree == null)
{
// Create the tree (implementation would depend on your requirements)
// For now, we'll just return an empty structure
tree = new MNode
{
Id = 0,
Name = treeName,
Children = []
};
}
return Ok(tree);
}
}
}

19
DTOs/MJournal.cs Normal file
View File

@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace public_valetax.DTOs
{
public class MJournal
{
[JsonPropertyName("text")]
public string Text { get; set; } = string.Empty;
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("eventId")]
public long EventId { get; set; }
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
}
}

16
DTOs/MJournalInfo.cs Normal file
View File

@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace public_valetax.DTOs
{
public class MJournalInfo
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("eventId")]
public long EventId { get; set; }
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
}
}

16
DTOs/MNode.cs Normal file
View File

@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace public_valetax.DTOs
{
public class MNode
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("children")]
public ICollection<MNode> Children { get; set; } = [];
}
}

View File

@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace public_valetax.DTOs
{
public class MRange_MJournalInfo
{
[JsonPropertyName("skip")]
public int Skip { get; set; }
[JsonPropertyName("count")]
public int Count { get; set; }
[JsonPropertyName("items")]
public ICollection<MJournalInfo> Items { get; set; } = [];
}
}

10
DTOs/TokenInfo.cs Normal file
View File

@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace public_valetax.DTOs
{
public class TokenInfo
{
[JsonPropertyName("token")]
public string Token { get; set; } = string.Empty;
}
}

16
DTOs/VJournalFilter.cs Normal file
View File

@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace public_valetax.DTOs
{
public class VJournalFilter
{
[JsonPropertyName("from")]
public DateTime? From { get; set; }
[JsonPropertyName("to")]
public DateTime? To { get; set; }
[JsonPropertyName("search")]
public string Search { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore;
using public_valetax.Models;
namespace public_valetax.Data
{
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
{
public DbSet<Tree> Trees { get; set; } = null!;
public DbSet<Node> Nodes { get; set; } = null!;
public DbSet<JournalEntry> JournalEntries { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure Tree entity
modelBuilder.Entity<Tree>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.HasIndex(e => e.Name).IsUnique();
});
// Configure Node entity
modelBuilder.Entity<Node>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
// Self-referencing foreign key for parent-child relationship
entity.HasOne(n => n.Parent)
.WithMany(n => n.Children)
.HasForeignKey(n => n.ParentId)
.OnDelete(DeleteBehavior.Cascade);
// Foreign key to Tree
entity.HasOne(n => n.Tree)
.WithMany(t => t.Nodes)
.HasForeignKey(n => n.TreeId)
.OnDelete(DeleteBehavior.Cascade);
// Ensure unique node names within the same tree
entity.HasIndex(n => new { n.TreeId, n.Name }).IsUnique();
});
// Configure JournalEntry entity
modelBuilder.Entity<JournalEntry>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.ExceptionType).IsRequired().HasMaxLength(255);
entity.Property(e => e.Message).IsRequired();
entity.HasIndex(e => e.EventId).IsUnique();
});
}
}
}

View File

@@ -0,0 +1,11 @@
namespace public_valetax.Exceptions
{
public class SecureException : Exception
{
public SecureException() : base() { }
public SecureException(string message) : base(message) { }
public SecureException(string message, Exception innerException) : base(message, innerException) { }
}
}

View File

@@ -0,0 +1,74 @@
using System.Text.Json;
using public_valetax.Exceptions;
using public_valetax.Repositories;
namespace public_valetax.Middleware
{
public class ExceptionHandlingMiddleware(RequestDelegate _next, ILogger<ExceptionHandlingMiddleware> _logger)
{
public async Task InvokeAsync(HttpContext context, IJournalRepository journalRepository)
{
try
{
if (context.Request.ContentLength > 0)
{
context.Request.EnableBuffering();
}
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
// Generate event ID
var eventId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
// Capture request details
var queryParameters = context.Request.QueryString.ToString();
string? bodyParameters = null;
// Try to capture body parameters (be careful not to consume the stream multiple times)
if (context.Request.ContentLength > 0)
{
context.Request.Body.Position = 0; // Reset for next middleware
using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
bodyParameters = await reader.ReadToEndAsync();
context.Request.Body.Position = 0; // Reset for next middleware
}
// Log to journal
await journalRepository.CreateJournalEntryAsync(
eventId,
ex.GetType().Name,
ex.Message,
queryParameters,
bodyParameters,
ex.StackTrace);
// Handle specific exception types
await HandleExceptionAsync(context, ex, eventId);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, long eventId)
{
var response = new
{
type = exception is SecureException ? "Secure" : "Exception",
id = eventId.ToString(),
data = new
{
message = exception is SecureException
? exception.Message
: $"Internal server error ID = {eventId}"
}
};
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
var jsonResponse = JsonSerializer.Serialize(response);
await context.Response.WriteAsync(jsonResponse);
}
}
}

View File

@@ -0,0 +1,10 @@
namespace public_valetax.Middleware
{
public static class ExceptionHandlingMiddlewareExtensions
{
public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ExceptionHandlingMiddleware>();
}
}
}

View File

@@ -0,0 +1,170 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using public_valetax.Data;
#nullable disable
namespace public_valetax.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251226122935_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("public_valetax.Models.JournalEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("BodyParameters")
.HasColumnType("text")
.HasColumnName("body_parameters");
b.Property<long>("EventId")
.HasColumnType("bigint")
.HasColumnName("event_id");
b.Property<string>("ExceptionType")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("exception_type");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text")
.HasColumnName("message");
b.Property<string>("QueryParameters")
.HasColumnType("text")
.HasColumnName("query_parameters");
b.Property<string>("StackTrace")
.HasColumnType("text")
.HasColumnName("stack_trace");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasColumnName("timestamp");
b.HasKey("Id");
b.HasIndex("EventId")
.IsUnique();
b.ToTable("journal_entries");
});
modelBuilder.Entity("public_valetax.Models.Node", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("name");
b.Property<long?>("ParentId")
.HasColumnType("bigint")
.HasColumnName("parent_id");
b.Property<long>("TreeId")
.HasColumnType("bigint")
.HasColumnName("tree_id");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("TreeId", "Name")
.IsUnique();
b.ToTable("nodes");
});
modelBuilder.Entity("public_valetax.Models.Tree", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("trees");
});
modelBuilder.Entity("public_valetax.Models.Node", b =>
{
b.HasOne("public_valetax.Models.Node", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("public_valetax.Models.Tree", "Tree")
.WithMany("Nodes")
.HasForeignKey("TreeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
b.Navigation("Tree");
});
modelBuilder.Entity("public_valetax.Models.Node", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("public_valetax.Models.Tree", b =>
{
b.Navigation("Nodes");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,113 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace public_valetax.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "journal_entries",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
event_id = table.Column<long>(type: "bigint", nullable: false),
timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
query_parameters = table.Column<string>(type: "text", nullable: true),
body_parameters = table.Column<string>(type: "text", nullable: true),
stack_trace = table.Column<string>(type: "text", nullable: true),
exception_type = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
message = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_journal_entries", x => x.id);
});
migrationBuilder.CreateTable(
name: "trees",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_trees", x => x.id);
});
migrationBuilder.CreateTable(
name: "nodes",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
tree_id = table.Column<long>(type: "bigint", nullable: false),
parent_id = table.Column<long>(type: "bigint", nullable: true),
name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_nodes", x => x.id);
table.ForeignKey(
name: "FK_nodes_nodes_parent_id",
column: x => x.parent_id,
principalTable: "nodes",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_nodes_trees_tree_id",
column: x => x.tree_id,
principalTable: "trees",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_journal_entries_event_id",
table: "journal_entries",
column: "event_id",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_nodes_parent_id",
table: "nodes",
column: "parent_id");
migrationBuilder.CreateIndex(
name: "IX_nodes_tree_id_name",
table: "nodes",
columns: new[] { "tree_id", "name" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_trees_name",
table: "trees",
column: "name",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "journal_entries");
migrationBuilder.DropTable(
name: "nodes");
migrationBuilder.DropTable(
name: "trees");
}
}
}

View File

@@ -0,0 +1,167 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using public_valetax.Data;
#nullable disable
namespace public_valetax.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("public_valetax.Models.JournalEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("BodyParameters")
.HasColumnType("text")
.HasColumnName("body_parameters");
b.Property<long>("EventId")
.HasColumnType("bigint")
.HasColumnName("event_id");
b.Property<string>("ExceptionType")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("exception_type");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text")
.HasColumnName("message");
b.Property<string>("QueryParameters")
.HasColumnType("text")
.HasColumnName("query_parameters");
b.Property<string>("StackTrace")
.HasColumnType("text")
.HasColumnName("stack_trace");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasColumnName("timestamp");
b.HasKey("Id");
b.HasIndex("EventId")
.IsUnique();
b.ToTable("journal_entries");
});
modelBuilder.Entity("public_valetax.Models.Node", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("name");
b.Property<long?>("ParentId")
.HasColumnType("bigint")
.HasColumnName("parent_id");
b.Property<long>("TreeId")
.HasColumnType("bigint")
.HasColumnName("tree_id");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("TreeId", "Name")
.IsUnique();
b.ToTable("nodes");
});
modelBuilder.Entity("public_valetax.Models.Tree", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("trees");
});
modelBuilder.Entity("public_valetax.Models.Node", b =>
{
b.HasOne("public_valetax.Models.Node", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("public_valetax.Models.Tree", "Tree")
.WithMany("Nodes")
.HasForeignKey("TreeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
b.Navigation("Tree");
});
modelBuilder.Entity("public_valetax.Models.Node", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("public_valetax.Models.Tree", b =>
{
b.Navigation("Nodes");
});
#pragma warning restore 612, 618
}
}
}

38
Models/JournalEntry.cs Normal file
View File

@@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace public_valetax.Models
{
[Table("journal_entries")]
public class JournalEntry
{
[Key]
[Column("id")]
public long Id { get; set; }
[Required]
[Column("event_id")]
public long EventId { get; set; }
[Required]
[Column("timestamp")]
public DateTime Timestamp { get; set; }
[Column("query_parameters")]
public string? QueryParameters { get; set; }
[Column("body_parameters")]
public string? BodyParameters { get; set; }
[Column("stack_trace")]
public string? StackTrace { get; set; }
[Required]
[Column("exception_type")]
public string ExceptionType { get; set; } = string.Empty;
[Required]
[Column("message")]
public string Message { get; set; } = string.Empty;
}
}

36
Models/Node.cs Normal file
View File

@@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace public_valetax.Models
{
[Table("nodes")]
public class Node
{
[Key]
[Column("id")]
public long Id { get; set; }
[Required]
[Column("tree_id")]
public long TreeId { get; set; }
[Column("parent_id")]
public long? ParentId { get; set; }
[Required]
[Column("name")]
public string Name { get; set; } = string.Empty;
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation properties
[ForeignKey("TreeId")]
public Tree Tree { get; set; } = null!;
[ForeignKey("ParentId")]
public Node? Parent { get; set; }
public ICollection<Node> Children { get; set; } = [];
}
}

23
Models/Tree.cs Normal file
View File

@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace public_valetax.Models
{
[Table("trees")]
public class Tree
{
[Key]
[Column("id")]
public long Id { get; set; }
[Required]
[Column("name")]
public string Name { get; set; } = string.Empty;
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation property
public ICollection<Node> Nodes { get; set; } = [];
}
}

41
Program.cs Normal file
View File

@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore;
using public_valetax.Data;
using public_valetax.Repositories;
using public_valetax.Middleware;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddOpenApi();
// Add Entity Framework
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add repositories
builder.Services.AddScoped<ITreeRepository, TreeRepository>();
builder.Services.AddScoped<IJournalRepository, JournalRepository>();
// Add controllers
builder.Services.AddControllers();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
// Add exception handling middleware
app.UseExceptionHandling();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5090",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7153;http://localhost:5090",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

55
README.md Normal file
View File

@@ -0,0 +1,55 @@
# Valetax Test Application
This is an ASP.NET Core 10 application with REST API that implements the requirements from the ValetaxTest.pdf document.
## Features Implemented
1. **Tree Node Management**
- Create independent trees with nodes
- Each node belongs to a single tree
- Child nodes belong to the same tree as their parent
- Unique node names within the same parent
- Delete nodes with all their descendants
- Rename nodes
2. **Exception Journal**
- Track all exceptions during REST API request processing
- Store unique Event ID, Timestamp, query/body parameters, and stack trace
- Custom SecureException handling
3. **API Endpoints**
- Journal API: getRange, getSingle
- Authentication API: rememberMe (simplified implementation)
- Tree API: get
- Node API: create, delete, rename
## Database Setup
The application uses PostgreSQL as the database. To set up the database:
1. Make sure you have PostgreSQL installed and running
2. Create a database named "valetax"
3. Update the connection string in appsettings.json if needed
## Running Migrations
To create and run the database migration, execute the following commands:
```bash
dotnet ef migrations add InitialCreate
dotnet ef database update
```
## Running the Application
To run the application:
```bash
dotnet run
```
The API will be available at https://localhost:7153 or http://localhost:5090.
## API Documentation
The API follows the Swagger specification provided in the requirements. You can access the OpenAPI documentation at `/openapi/v1.json` when running in development mode.

View File

@@ -0,0 +1,19 @@
using public_valetax.Models;
using public_valetax.DTOs;
namespace public_valetax.Repositories
{
public interface IJournalRepository
{
Task<JournalEntry> CreateJournalEntryAsync(
long eventId,
string exceptionType,
string message,
string? queryParameters = null,
string? bodyParameters = null,
string? stackTrace = null);
Task<MJournal?> GetJournalEntryAsync(long id);
Task<MRange_MJournalInfo> GetJournalEntriesRangeAsync(int skip, int take, VJournalFilter filter);
}
}

View File

@@ -0,0 +1,16 @@
using public_valetax.Models;
using public_valetax.DTOs;
namespace public_valetax.Repositories
{
public interface ITreeRepository
{
Task<Tree?> GetTreeByNameAsync(string name);
Task<Tree> CreateTreeAsync(string name);
Task<Node?> GetNodeByIdAsync(long nodeId);
Task<Node> CreateNodeAsync(long treeId, long? parentId, string name);
Task DeleteNodeAsync(long nodeId);
Task RenameNodeAsync(long nodeId, string newName);
Task<MNode?> GetTreeStructureAsync(string treeName);
}
}

View File

@@ -0,0 +1,96 @@
using Microsoft.EntityFrameworkCore;
using public_valetax.Data;
using public_valetax.Models;
using public_valetax.DTOs;
namespace public_valetax.Repositories
{
public class JournalRepository(ApplicationDbContext _context) : IJournalRepository
{
public async Task<JournalEntry> CreateJournalEntryAsync(
long eventId,
string exceptionType,
string message,
string? queryParameters = null,
string? bodyParameters = null,
string? stackTrace = null)
{
var journalEntry = new JournalEntry
{
EventId = eventId,
Timestamp = DateTime.UtcNow,
ExceptionType = exceptionType,
Message = message,
QueryParameters = queryParameters,
BodyParameters = bodyParameters,
StackTrace = stackTrace
};
_context.JournalEntries.Add(journalEntry);
await _context.SaveChangesAsync();
return journalEntry;
}
public async Task<MJournal?> GetJournalEntryAsync(long id)
{
var journalEntry = await _context.JournalEntries.FindAsync(id);
if (journalEntry == null)
{
return null;
}
return new MJournal
{
Id = journalEntry.Id,
EventId = journalEntry.EventId,
Text = journalEntry.Message,
CreatedAt = journalEntry.Timestamp
};
}
public async Task<MRange_MJournalInfo> GetJournalEntriesRangeAsync(int skip, int take, VJournalFilter filter)
{
var query = _context.JournalEntries.AsQueryable();
// Apply filters if provided
if (filter.From.HasValue)
{
query = query.Where(j => j.Timestamp >= filter.From.Value);
}
if (filter.To.HasValue)
{
query = query.Where(j => j.Timestamp <= filter.To.Value);
}
if (!string.IsNullOrEmpty(filter.Search))
{
query = query.Where(j => j.Message.Contains(filter.Search) || j.ExceptionType.Contains(filter.Search));
}
// Get total count before pagination
var totalCount = await query.CountAsync();
// Apply pagination
var journalEntries = await query
.OrderByDescending(j => j.Timestamp)
.Skip(skip)
.Take(take)
.Select(j => new MJournalInfo
{
Id = j.Id,
EventId = j.EventId,
CreatedAt = j.Timestamp
})
.ToListAsync();
return new MRange_MJournalInfo
{
Skip = skip,
Count = totalCount,
Items = journalEntries
};
}
}
}

View File

@@ -0,0 +1,168 @@
using Microsoft.EntityFrameworkCore;
using public_valetax.Data;
using public_valetax.Models;
using public_valetax.DTOs;
using public_valetax.Exceptions;
namespace public_valetax.Repositories
{
public class TreeRepository(ApplicationDbContext _context) : ITreeRepository
{
public async Task<Tree?> GetTreeByNameAsync(string name)
{
return await _context.Trees
.Include(t => t.Nodes)
.FirstOrDefaultAsync(t => t.Name == name);
}
public async Task<Tree> CreateTreeAsync(string name)
{
var tree = new Tree
{
Name = name,
CreatedAt = DateTime.UtcNow
};
if (await _context.Trees.AnyAsync(t => t.Name == name))
{
throw new SecureException("Tree with such name already exist");
}
_context.Trees.Add(tree);
await _context.SaveChangesAsync();
return tree;
}
public async Task<Node?> GetNodeByIdAsync(long nodeId)
{
return await _context.Nodes.FindAsync(nodeId);
}
public async Task<Node> CreateNodeAsync(long treeId, long? parentId, string name)
{
// Verify that parent node belongs to the same tree if provided
if (parentId.HasValue)
{
var parentNode = await _context.Nodes.FindAsync(parentId.Value);
if (parentNode == null || parentNode.TreeId != treeId)
{
throw new SecureException("Parent node does not belong to the specified tree");
}
}
// Check if a node with the same name already exists in the same tree
var existingNode = await _context.Nodes
.FirstOrDefaultAsync(n => n.TreeId == treeId && n.Name == name && n.ParentId == parentId);
if (existingNode != null)
{
throw new SecureException("A node with the same name already exists in this location");
}
var node = new Node
{
TreeId = treeId,
ParentId = parentId,
Name = name,
CreatedAt = DateTime.UtcNow
};
_context.Nodes.Add(node);
await _context.SaveChangesAsync();
return node;
}
public async Task DeleteNodeAsync(long nodeId)
{
var node = await _context.Nodes
.Include(n => n.Children)
.FirstOrDefaultAsync(n => n.Id == nodeId);
if (node == null)
{
throw new SecureException("Node not found");
}
// Check if node has children
if (node.Children.Any())
{
throw new SecureException("You have to delete all children nodes first");
}
_context.Nodes.Remove(node);
await _context.SaveChangesAsync();
}
public async Task RenameNodeAsync(long nodeId, string newName)
{
var node = await _context.Nodes.FindAsync(nodeId);
if (node == null)
{
throw new SecureException("Node not found");
}
// Check if another node with the same name already exists in the same parent
var existingNode = await _context.Nodes
.FirstOrDefaultAsync(n => n.TreeId == node.TreeId && n.Name == newName && n.ParentId == node.ParentId && n.Id != nodeId);
if (existingNode != null)
{
throw new SecureException("A node with the same name already exists in this location");
}
node.Name = newName;
await _context.SaveChangesAsync();
}
public async Task<MNode?> GetTreeStructureAsync(string treeName)
{
var tree = await _context.Trees
.Include(t => t.Nodes)
.FirstOrDefaultAsync(t => t.Name == treeName);
if (tree == null)
{
return null;
}
// Build the tree structure recursively
var rootNodes = tree.Nodes
.Where(n => n.ParentId == null)
.Select(n => BuildMNode(n, tree.Nodes))
.ToList();
// If no root node exists, create one
if (!rootNodes.Any())
{
return new MNode
{
Id = 0,
Name = tree.Name,
Children = []
};
}
return rootNodes.First();
}
private MNode BuildMNode(Node node, ICollection<Node> allNodes)
{
var mNode = new MNode
{
Id = node.Id,
Name = node.Name,
Children = []
};
// Add children recursively
var children = allNodes.Where(n => n.ParentId == node.Id).ToList();
foreach (var child in children)
{
mNode.Children.Add(BuildMNode(child, allNodes));
}
return mNode;
}
}
}

View File

@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5433;Database=valetax;Username=postgres;Password=abc505050123XXX"
}
}

8
appsettings.json Normal file
View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

BIN
docs/ValetaxTest.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1,257 @@
swagger: '2.0'
info:
title: Swagger
version: 0.0.1
tags:
- name: user.journal
description: Represents journal API
- name: user.partner
description: Represents auth API
- name: user.tree
description: Represents entire tree API
- name: user.tree.node
description: Represents tree node API
paths:
/api.user.journal.getRange:
post:
summary: ''
description: >-
Provides the pagination API. Skip means the number of items should be
skipped by server. Take means the maximum number items should be
returned by server. All fields of the filter are optional.
tags:
- user.journal
parameters:
- in: query
name: skip
required: true
type: integer
format: int32
- in: query
name: take
required: true
type: integer
format: int32
- in: body
name: filter
required: false
schema:
type: object
responses:
'200':
schema:
$ref: '#/definitions/FxNet.Test.Model.MRange_MJournalInfo'
description: Successful response
/api.user.journal.getSingle:
post:
summary: ''
description: Returns the information about an particular event by ID.
tags:
- user.journal
parameters:
- in: query
name: id
required: true
type: integer
format: int64
responses:
'200':
schema:
$ref: '#/definitions/FxNet.Test.Model.MJournal'
description: Successful response
/api.user.partner.rememberMe:
post:
summary: ''
description: >-
(Optional) Saves user by unique code and returns auth token required on all other requests, if implemented.
tags:
- user.partner
parameters:
- in: query
name: code
required: true
type: string
format: string
responses:
'200':
schema:
$ref: '#/definitions/FxNet.Test.Model.TokenInfo'
description: Successful response
/api.user.tree.get:
post:
summary: ''
description: >-
Returns your entire tree. If your tree doesn't exist it will be created
automatically.
tags:
- user.tree
parameters:
- in: query
name: treeName
required: true
type: string
format: string
responses:
'200':
schema:
$ref: '#/definitions/FxNet.Test.Model.MNode'
description: Successful response
/api.user.tree.node.create:
post:
summary: ''
description: >-
Create a new node in your tree. You must to specify a parent node ID
that belongs to your tree or dont pass parent ID to create tree first level node. A new node name must be unique across all
siblings.
tags:
- user.tree.node
parameters:
- in: query
name: treeName
required: true
type: string
format: string
- in: query
name: parentNodeId
required: false
type: integer
format: int64
- in: query
name: nodeName
required: true
type: string
format: string
responses:
'200':
description: Successful response
/api.user.tree.node.delete:
post:
summary: ''
description: >-
Delete an existing node and all its descendants
tags:
- user.tree.node
parameters:
- in: query
name: nodeId
required: true
type: integer
format: int64
responses:
'200':
description: Successful response
/api.user.tree.node.rename:
post:
summary: ''
description: >-
Rename an existing node in your tree. A new name of the node must be unique across all
siblings.
tags:
- user.tree.node
parameters:
- in: query
name: nodeId
required: true
type: integer
format: int64
- in: query
name: newNodeName
required: true
type: string
format: string
responses:
'200':
description: Successful response
definitions:
FxNet.Test.Model.MJournal:
properties:
text:
type: string
format: string
id:
type: integer
format: int64
eventId:
type: integer
format: int64
createdAt:
type: string
format: datetime
example: '2025-05-23T12:18:16.9222634Z'
required:
- text
- id
- eventId
- createdAt
FxNet.Test.Model.MJournalInfo:
properties:
id:
type: integer
format: int64
eventId:
type: integer
format: int64
createdAt:
type: string
format: datetime
example: '2025-05-23T12:18:16.922346Z'
required:
- id
- eventId
- createdAt
FxNet.Test.View.VJournalFilter:
properties:
from:
type: string
format: datetime
example: '2025-05-23T12:18:16.9223615Z'
to:
type: string
format: datetime
example: '2025-05-23T12:18:16.9223726Z'
search:
type: string
format: string
required:
- search
FxNet.Test.Model.MNode:
properties:
id:
type: integer
format: int64
name:
type: string
format: string
children:
type: array
items:
$ref: '#/definitions/FxNet.Test.Model.MNode'
required:
- id
- name
- children
FxNet.Test.Model.MRange_MJournalInfo:
properties:
skip:
type: integer
format: int32
count:
type: integer
format: int32
items:
type: array
items:
$ref: '#/definitions/FxNet.Test.Model.MJournalInfo'
required:
- skip
- count
- items
FxNet.Test.Model.TokenInfo:
properties:
token:
type: string
format: string
required:
- skip
- count
- items

18
public-valetax.csproj Normal file
View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>public_valetax</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1" />
<PackageReference Include="Scalar.AspNetCore" Version="2.11.10" />
</ItemGroup>
</Project>

32
public-valetax.http Normal file
View File

@@ -0,0 +1,32 @@
@hostname = localhost
@port = 5090
@host = {{hostname}}:{{port}}
### Get Tree
POST http://{{host}}/api.tree/get?treeName=MyTree
### Create Root Node
POST http://{{host}}/api.node/create?treeName=MyTree&nodeName=RootNode
### Create Child Node
POST http://{{host}}/api.node/create?treeName=MyTree&parentNodeId=1&nodeName=ChildNode
### Rename Node
POST http://{{host}}/api.node/rename?nodeId=1&newNodeName=NewRootName
### Delete Node
POST http://{{host}}/api.node/delete?nodeId=2
### Get Journal Entries Range
POST http://{{host}}/api.journal/getRange?skip=0&take=10
Content-Type: application/json
{
"search": ""
}
### Get Single Journal Entry
POST http://{{host}}/api.journal/getSingle?id=1
### Authenticate (Optional)
POST http://{{host}}/api.partner/rememberMe?code=unique_code

24
public-valetax.sln Normal file
View File

@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "public-valetax", "public-valetax.csproj", "{20B5CC40-8AC1-19AE-8AE8-DE856E2342D0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{20B5CC40-8AC1-19AE-8AE8-DE856E2342D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{20B5CC40-8AC1-19AE-8AE8-DE856E2342D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{20B5CC40-8AC1-19AE-8AE8-DE856E2342D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{20B5CC40-8AC1-19AE-8AE8-DE856E2342D0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DB931A39-1D59-483D-A80F-B7AAD3F311B4}
EndGlobalSection
EndGlobal

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB