diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..af50df1
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,25 @@
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/.idea
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ab4f3e0
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,231 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = false
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false
+dotnet_style_qualification_for_field = false
+dotnet_style_qualification_for_method = false
+dotnet_style_qualification_for_property = false
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_namespace_match_folder = true
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true
+dotnet_style_prefer_collection_expression = when_types_loosely_match
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:silent
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = true
+dotnet_style_allow_statement_immediately_after_block_experimental = true
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false
+csharp_style_var_for_built_in_types = false
+csharp_style_var_when_type_is_apparent = false
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true
+csharp_style_expression_bodied_constructors = false
+csharp_style_expression_bodied_indexers = true
+csharp_style_expression_bodied_lambdas = true
+csharp_style_expression_bodied_local_functions = false
+csharp_style_expression_bodied_methods = false
+csharp_style_expression_bodied_operators = false
+csharp_style_expression_bodied_properties = true
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true
+csharp_style_pattern_matching_over_is_with_cast_check = true
+csharp_style_prefer_extended_property_pattern = true
+csharp_style_prefer_not_pattern = true
+csharp_style_prefer_pattern_matching = true
+csharp_style_prefer_switch_expression = true
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true
+
+# Modifier preferences
+csharp_prefer_static_local_function = true
+csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async
+csharp_style_prefer_readonly_struct = true
+csharp_style_prefer_readonly_struct_member = true
+
+# Code-block preferences
+csharp_prefer_braces = true
+csharp_prefer_simple_using_statement = true
+csharp_style_namespace_declarations = block_scoped
+csharp_style_prefer_method_group_conversion = true
+csharp_style_prefer_primary_constructors = true
+csharp_style_prefer_top_level_statements = true
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true
+csharp_style_deconstructed_variable_declaration = true
+csharp_style_implicit_object_creation_when_type_is_apparent = true
+csharp_style_inlined_variable_declaration = true
+csharp_style_prefer_index_operator = true
+csharp_style_prefer_local_over_anonymous_function = true
+csharp_style_prefer_null_check_over_type_check = true
+csharp_style_prefer_range_operator = true
+csharp_style_prefer_tuple_swap = true
+csharp_style_prefer_utf8_string_literals = true
+csharp_style_throw_expression = true
+csharp_style_unused_value_assignment_preference = discard_variable
+csharp_style_unused_value_expression_statement_preference = discard_variable
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace
+
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
+csharp_style_allow_embedded_statements_on_same_line_experimental = true
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..3983971
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,26 @@
+# App
+ASPNETCORE_ENVIRONMENT=Development
+
+# Oracle
+ORACLE_PASSWORD='8TYd50g*0trE'
+ORACLE_APP_USER='oracle'
+ORACLE_APP_USER_PASSWORD='9%8bphDFOj4P'
+
+# Postgres
+POSTGRES_DB=pyrevit-telemetry
+POSTGRES_USER='postgres'
+POSTGRES_PASSWORD='iBT|vC857AoS'
+
+# MSSQL
+MSSQL_ACCEPT_EULA=Y
+MSSQL_DATABASE_NAME=pyrevit-telemetry
+MSSQL_USER='sa'
+MSSQL_SA_PASSWORD='AwbW~PNM3NxH'
+
+# SQLite
+SQLITE_DATABASE_NAME=pyrevit-telemetry
+
+# MongoDB
+MONGODB_DATABASE_NAME=pyrevit-telemetry
+MONGODB_ROOT_USER='mongodb'
+MONGODB_ROOT_PASSWORD='xF?iqMJhZj$R'
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1beb0d1
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,64 @@
+
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..0e81b91
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,73 @@
+name: Tests
+
+on:
+ pull_request:
+ branches: [ main ]
+
+env:
+ BUILD_PATH: bin
+ BUILD_CONFIGURATION: Debug
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: |
+ dotnet build "./src/Telemetry.Api/Telemetry.Api.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+ dotnet build "./src/Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+ dotnet build "./src/Telemetry.Migrations.Postgres/Telemetry.Migrations.Postgres.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+ dotnet build "./src/Telemetry.Migrations.Sqlite/Telemetry.Migrations.Sqlite.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+ dotnet build "./src/Telemetry.Migrations.SqlServer/Telemetry.Migrations.SqlServer.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+
+
+ - name: Upload Build Results
+ uses: actions/upload-artifact@v7
+ with:
+ name: build
+ path: ${{ env.BUILD_PATH }}
+
+ docker-publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - name: Generate a version
+ run: echo "VERSION=2.0.0.${{ github.run_number }}-alpha" >> $GITHUB_ENV
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v4
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Build and push
+ uses: docker/build-push-action@v7
+ with:
+ push: true
+ context: .
+ file: src/Telemetry.Api/Dockerfile
+ tags: pyrevitlabs/pyrevit-telemetry:alpha
+
+ - name: Build and push
+ uses: docker/build-push-action@v7
+ with:
+ push: true
+ context: .
+ file: src/Telemetry.Api/Dockerfile
+ tags: pyrevitlabs/pyrevit-telemetry:${{ env.VERSION }}
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..983376e
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,107 @@
+name: Tests
+
+on:
+ push:
+ branches: [ main ]
+
+env:
+ BUILD_PATH: /bin
+ BUILD_CONFIGURATION: Release
+ PUBLISH_PATH: /publish
+ MIGRATIONS_PATH: migrations
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Generate version
+ run: echo "VERSION=2.0.0.${{ github.run_number }}" >> $GITHUB_ENV
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: |
+ dotnet build "./src/Telemetry.Api/Telemetry.Api.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+ dotnet build "./src/Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+ dotnet build "./src/Telemetry.Migrations.Postgres/Telemetry.Migrations.Postgres.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+ dotnet build "./src/Telemetry.Migrations.Sqlite/Telemetry.Migrations.Sqlite.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+ dotnet build "./src/Telemetry.Migrations.SqlServer/Telemetry.Migrations.SqlServer.csproj" -c $BUILD_CONFIGURATION -o $BUILD_PATH
+
+ - name: Publish
+ run: |
+ dotnet publish "./src/Telemetry.Api/Telemetry.Api.csproj" -c $BUILD_CONFIGURATION -o $PUBLISH_PATH
+ dotnet publish "./src/Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj" -c $BUILD_CONFIGURATION -o $MIGRATIONS_PATH/oracle
+ dotnet publish "./src/Telemetry.Migrations.Postgres/Telemetry.Migrations.Postgres.csproj" -c $BUILD_CONFIGURATION -o $MIGRATIONS_PATH/postgres
+ dotnet publish "./src/Telemetry.Migrations.Sqlite/Telemetry.Migrations.Sqlite.csproj" -c $BUILD_CONFIGURATION -o $MIGRATIONS_PATH/sqlite
+ dotnet publish "./src/Telemetry.Migrations.SqlServer/Telemetry.Migrations.SqlServer.csproj" -c $BUILD_CONFIGURATION -o $MIGRATIONS_PATH/sqlserver
+
+ - name: Upload Publish Results
+ uses: actions/upload-artifact@v7
+ with:
+ name: publish
+ path: |
+ ${{ env.BUILD_PATH }}/*.*
+ ${{ env.MIGRATIONS_PATH }}/oracle/Telemetry.Migrations.Oracle.dll
+ ${{ env.MIGRATIONS_PATH }}/postgres/Telemetry.Migrations.Postgres.dll
+ ${{ env.MIGRATIONS_PATH }}/sqlite/Telemetry.Migrations.Sqlite.dll
+ ${{ env.MIGRATIONS_PATH }}/sqlserver/Telemetry.Migrations.SqlServer.dll
+
+ - name: Publish Release
+ uses: softprops/action-gh-release@v2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ draft: true
+ prerelease: false
+ body: dont forget fill changelog
+ tag_name: ${{ env.VERSION }}
+ name: pyrevit-telemetry ${{ env.VERSION }}
+ files: |
+ ${{ env.BUILD_PATH }}/*.*
+ ${{ env.MIGRATIONS_PATH }}/oracle/Telemetry.Migrations.Oracle.dll
+ ${{ env.MIGRATIONS_PATH }}/postgres/Telemetry.Migrations.Postgres.dll
+ ${{ env.MIGRATIONS_PATH }}/sqlite/Telemetry.Migrations.Sqlite.dll
+ ${{ env.MIGRATIONS_PATH }}/sqlserver/Telemetry.Migrations.SqlServer.dll
+
+ docker-publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - name: Generate version
+ run: echo "VERSION=2.0.0.${{ github.run_number }}" >> $GITHUB_ENV
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v4
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Build and push
+ uses: docker/build-push-action@v7
+ with:
+ push: true
+ context: .
+ file: src/Telemetry.Api/Dockerfile
+ tags: pyrevitlabs/pyrevit-telemetry:latest
+
+ - name: Build and push
+ uses: docker/build-push-action@v7
+ with:
+ push: true
+ context: .
+ file: src/Telemetry.Api/Dockerfile
+ tags: pyrevitlabs/pyrevit-telemetry:${{ env.VERSION }}
\ No newline at end of file
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..016f105
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,41 @@
+name: Tests
+
+on:
+ pull_request:
+ branches: [ main ]
+
+env:
+ BUILD_PATH: bin
+ BUILD_CONFIGURATION: Debug
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore -c ${{ env.BUILD_CONFIGURATION }}
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Test
+ run: dotnet test --no-build -c ${{ env.BUILD_CONFIGURATION }}
+
+ - name: Upload Test Results
+ uses: actions/upload-artifact@v7
+ with:
+ name: test-results
+ path: '**/*-report.html'
diff --git a/.gitignore b/.gitignore
index 6f72f89..62da786 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,25 +1,403 @@
-# If you prefer the allow list template instead of the deny list, see community template:
-# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
-#
-# Binaries for programs and plugins
-*.exe
-*.exe~
-*.dll
-*.so
-*.dylib
-
-# Test binary, built with `go test -c`
-*.test
-
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
-# Dependency directories (remove the comment below to include it)
-# vendor/
-
-# Go workspace file
-go.work
-go.work.sum
-
-# env file
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# 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/
+[Oo]ut/
+[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 Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# 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
+*.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 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/
+
+# 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
+
+# Default ignored files
+/shelf/
+/workspace.xml
+
+# Rider ignored files
+/.idea.dosymep.iml
+/modules.xml
+/projectSettingsUpdater.xml
+/contentModel.xml
+
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+
+# User specific
+**/.idea/**/workspace.xml
+**/.idea/**/tasks.xml
+**/.idea/shelf/*
+**/.idea/dictionaries
+**/.idea/httpRequests/
+
+# Sensitive or high-churn files
+**/.idea/**/dataSources/
+**/.idea/**/dataSources.ids
+**/.idea/**/dataSources.xml
+**/.idea/**/dataSources.local.xml
+**/.idea/**/sqlDataSources.xml
+**/.idea/**/dynamic.xml
+
+# Rider
+# Rider auto-generates .iml files, and contentModel.xml
+**/.idea/**/*.iml
+**/.idea/**/contentModel.xml
+**/.idea/**/modules.xml
+
+/.idea
+/mongodb
.env
+/telemetry-db
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..0fa13a8
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at hello@pyrevitlabs.io. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..cb960b4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1 @@
+[See Contributing to pyRevit Source on pyRevit Wiki](https://pyrevitlabs.notion.site/Contributing-to-pyRevit-Source-6d25a5ac62c44bd9a0e9aefb8debefa2?pvs=4)
\ No newline at end of file
diff --git a/CREDITS.md b/CREDITS.md
new file mode 100644
index 0000000..964d749
--- /dev/null
+++ b/CREDITS.md
@@ -0,0 +1,3 @@
+## Credits
+
+[See Credits Here](http://credits.pyrevitlabs.io)
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..0d3e88d
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+ 2.0.0
+ 2.0.0.0
+ 2.0.0.0
+
+
\ No newline at end of file
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000..c1df222
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..46370ff
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,36 @@
+
+
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE.md
similarity index 100%
rename from LICENSE
rename to LICENSE.md
diff --git a/README.md b/README.md
index 3e09386..0a90a88 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,273 @@
-# telemetry-server
-Telemetry Server for pyRevit
+# Telemetry Server for pyRevit
+
+A high-performance .NET 10 Web API service designed to collect and store telemetry data
+from [pyRevit](https://github.com/pyrevitlabs/pyRevit). It supports multiple database providers and can be easily
+deployed using Docker.
+
+## Project Structure
+
+The project follows a Clean Architecture approach:
+
+- **src/Telemetry.Api**: Main application source code.
+ - **Application**: Business logic, DTOs, mappings, and service interfaces.
+ - **Domain**: Core domain models and entities.
+ - **Infrastructure**: Database persistence, Entity Framework Core contexts, and external service implementations.
+ - **Web**: ASP.NET Core controllers, middleware, and API configuration.
+- **src/Telemetry.Migrations.***: Separate projects for Entity Framework Core migrations (PostgreSQL, SQL Server, SQLite, Oracle).
+- **tests**: Comprehensive test suite using [TUnit](https://github.com/thomhurst/TUnit).
+ - **Telemetry.Api.UnitTests**: Isolated tests for business logic and mappings.
+ - **Telemetry.Api.IntegrationTests**: End-to-end tests for API endpoints and database interactions.
+- **telemetry-db**: Local directory for database persistence (used by Docker).
+
+## Prerequisites
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
+- [Docker](https://www.docker.com/get-started) and [Docker Compose](https://docs.docker.com/compose/install/)
+
+## Compilation
+
+To build the project locally, run the following command from the root directory:
+
+```bash
+dotnet build telemetry-server.slnx
+```
+
+To run the application:
+
+```bash
+dotnet run --project src/Telemetry.Api/Telemetry.Api.csproj
+```
+
+## Docker Deployment
+
+The project provides several Docker Compose configurations to support different database backends.
+
+### General Usage
+
+To start the API using the pre-built image, you can pull the latest version:
+
+```bash
+docker pull pyrevit-telemetry:latest
+```
+
+To start the API with a specific database, use the following command structure:
+
+```bash
+docker compose -f docker-compose.yml -f docker-compose..yml up -d
+```
+
+### Supported Database Variants
+
+| Database | Command |
+|:---------------|:----------------------------------------------------------------------------|
+| **PostgreSQL** | `docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d` |
+| **SQL Server** | `docker compose -f docker-compose.yml -f docker-compose.mssql.yml up -d` |
+| **SQLite** | `docker compose -f docker-compose.yml -f docker-compose.sqlite.yml up -d` |
+| **Oracle** | `docker compose -f docker-compose.yml -f docker-compose.oracle.yml up -d` |
+| **MongoDB** | `docker compose -f docker-compose.yml -f docker-compose.mongodb.yml up -d` |
+
+> ❗IMPORTANT
+> - **MySQL**: Support is currently unavailable because the necessary Entity Framework Core libraries have not yet been ported to .NET 10.
+> - **SQL Server & Oracle**: While these providers are implemented, they have not been fully tested in a .NET 10 environment. Full verification and ongoing support for these databases are left to the community.
+
+### Environment Variables
+
+Each database variant requires specific environment variables (usually defined in a `.env` file or passed directly). Key
+variables include:
+
+- `DbProvider`: Specifies the database provider (e.g., `postgres`, `mssql`, `sqlite`, `oracle`, `mongodb`).
+- `ConnectionStrings__DefaultConnection`: The connection string for the selected database.
+- `ASPNETCORE_HTTP_PORTS`: The port the API listens to on (default: `8080`).
+
+## API Endpoints
+
+The API is versioned (currently `v2`).
+
+- `POST /api/v2/scripts`: Submit pyRevit script execution telemetry.
+- `POST /api/v2/events`: Submit pyRevit event telemetry.
+- `GET /api/v2/status`: Check service and database connection status.
+- `GET /metrics`: Prometheus metrics for monitoring.
+- `GET /swagger`: OpenAPI/Swagger documentation (in Development mode).
+
+## API Documentation
+
+When running in **Development** mode, the application provides interactive API documentation using [Swagger (OpenAPI)](https://swagger.io/).
+
+To access the Swagger UI:
+1. Start the application in Development mode (`ASPNETCORE_ENVIRONMENT=Development`).
+2. Navigate to `http://localhost:8080/swagger` (or your configured port).
+
+From the Swagger UI, you can explore all available endpoints, view request/response schemas, and test the API directly from your browser.
+
+## Monitoring
+
+The application exposes Prometheus metrics at the `/metrics` endpoint. This can be used to monitor the service's health, request rates, and performance using [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/).
+
+To view the metrics:
+1. Start the application.
+2. Navigate to `http://localhost:8080/metrics` (or your configured port).
+
+## Testing
+
+The project uses [TUnit](https://github.com/thomhurst/TUnit), a modern testing framework for .NET, taking advantage of
+the latest features in .NET 10.
+
+### Unit Tests
+
+Unit tests focus on isolated business logic and data mappings. They do not require any external dependencies.
+
+To run unit tests:
+
+```bash
+dotnet run --project tests/Telemetry.Api.UnitTests/Telemetry.Api.UnitTests.csproj
+```
+
+### Integration Tests
+
+Integration tests verify the end-to-end functionality of the API, including database persistence. These tests
+use [Testcontainers](https://testcontainers.com/) to spin up actual database instances (except for SQLite) during the
+test execution.
+
+**Prerequisites:** Docker must be running on your machine to execute integration tests for PostgreSQL, SQL Server,
+Oracle, and MongoDB.
+
+To run integration tests:
+
+```bash
+dotnet run --project tests/Telemetry.Api.IntegrationTests/Telemetry.Api.IntegrationTests.csproj
+```
+
+## Manual Deployment (Without Docker)
+
+To run the server manually, follow these steps to build, configure, and publish the application.
+
+### Build and Publish
+
+1. **Publish the project:**
+ Run the following command to create a self-contained or framework-dependent deployment:
+
+ ```bash
+ dotnet publish src/Telemetry.Api/Telemetry.Api.csproj -c Release -o ./publish
+ ```
+
+2. **Navigate to the publish directory:**
+
+ ```bash
+ cd ./publish
+ ```
+
+### Configuration (appsettings.json)
+
+Before running the server, you must configure the database provider and connection string in `appsettings.json`.
+
+1. **Open `appsettings.json`** and add the following configuration:
+
+ ```json
+ {
+ "DbProvider": "postgres",
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=localhost;Database=telemetry;Username=postgres;Password=password"
+ },
+ "MongoDbDatabaseName": "telemetry",
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+ }
+ ```
+
+2. **Configuration Options:**
+ - `DbProvider`: Set to one of `postgres`, `mssql`, `sqlite`, `oracle`, `mongodb`.
+ - `ConnectionStrings:DefaultConnection`: Provide the appropriate connection string for your database.
+ - `MongoDbDatabaseName`: (Optional) Required only if using `mongodb`.
+
+### Running the Server
+
+Start the application using the `dotnet` CLI:
+
+```bash
+dotnet Telemetry.Api.dll
+```
+
+The server will start and listen on the default ports (usually `http://localhost:5000` or as configured via environment variables).
+
+## Database Migrations
+
+The project uses Entity Framework Core (EF Core) to manage relational database schemas (PostgreSQL, SQL Server, SQLite, Oracle).
+
+### Prerequisites
+
+You need the `dotnet-ef` tool installed globally:
+
+```bash
+dotnet tool install --global dotnet-ef
+```
+
+### Creating a New Migration
+
+If you modify the data models in the `Domain` or `Infrastructure` layers, you need to generate a new migration for each supported database provider. Each database has its own project for migrations.
+
+To generate migrations correctly, set the required environment variables for the target database and run the `dotnet ef migrations add` command from the root directory.
+
+#### Oracle
+```bash
+DbProvider=oracle ConnectionStrings__DefaultConnection="Data Source=localhost:1521/xe;User Id=test;Password=test" \
+dotnet ef migrations add \
+ --project src/Telemetry.Migrations.Oracle \
+ --startup-project src/Telemetry.Api \
+ --context ApplicationDbContext
+```
+
+#### PostgreSQL
+```bash
+DbProvider=postgres ConnectionStrings__DefaultConnection="Host=localhost;Database=test;Username=test;Password=test" \
+dotnet ef migrations add \
+ --project src/Telemetry.Migrations.Postgres \
+ --startup-project src/Telemetry.Api \
+ --context ApplicationDbContext
+```
+
+#### SQLite
+```bash
+DbProvider=sqlite ConnectionStrings__DefaultConnection="Data Source=/app/data/telemetry.db" \
+dotnet ef migrations add \
+ --project src/Telemetry.Migrations.Sqlite \
+ --startup-project src/Telemetry.Api \
+ --context ApplicationDbContext
+```
+
+#### SQL Server
+```bash
+DbProvider=mssql ConnectionStrings__DefaultConnection="Server=localhost;Database=test;User Id=sa;Password=test" \
+dotnet ef migrations add \
+ --project src/Telemetry.Migrations.SqlServer \
+ --startup-project src/Telemetry.Api \
+ --context ApplicationDbContext
+```
+
+### Reviewing the generated files
+
+New migration files will be created in their respective projects under the `Migrations` directory (e.g., `src/Telemetry.Migrations.Postgres/Migrations`).
+
+### Applying Migrations
+
+- **Automatic:** The server automatically applies pending migrations on startup for all relational database providers.
+- **Manual:** You can manually update the database using the following command (example for SQLite):
+
+ ```bash
+ DbProvider=sqlite ConnectionStrings__DefaultConnection="Data Source=/app/data/telemetry.db" \
+ dotnet ef database update \
+ --project src/Telemetry.Migrations.Sqlite \
+ --startup-project src/Telemetry.Api \
+ --context ApplicationDbContext
+ ```
+
+> ❗NOTE
+> **MongoDB** does not use EF Core migrations. Changes to the MongoDB data structure are handled by the application at runtime (schema-less or through driver-level configuration).
+
+## License
+
+This project is licensed under the terms of the license included in the root directory.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..1f4d26f
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,22 @@
+# Security Policy
+
+> Note that the most up to date code is located in the develop branch. Every 6 months to 12 months, the develop branch
+> is merged to the master branch and new releases are integrated.
+
+## Reporting a Vulnerability
+
+**Please do not report security vulnerabilities through public pyRevit GitHub issues, discussions, or pull requests.**
+
+Instead, please send an email to eirannejad+pyrevitsecurity[@]gmail.com .
+
+Please include as much of the information listed below as you can to help us better understand and resolve the issue:
+
+- The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
+- Full paths of source file(s) related to the manifestation of the issue
+- The location of the affected source code (tag/branch/commit or direct URL)
+- Any special configuration required to reproduce the issue
+- Step-by-step instructions to reproduce the issue
+- Proof-of-concept or exploit code (if possible)
+- Impact of the issue, including how an attacker might exploit the issue
+
+This information will help us triage your report more quickly.
\ No newline at end of file
diff --git a/docker-compose.mongodb.yml b/docker-compose.mongodb.yml
new file mode 100644
index 0000000..e00011b
--- /dev/null
+++ b/docker-compose.mongodb.yml
@@ -0,0 +1,26 @@
+services:
+ telemetry-api:
+ environment:
+ - DbProvider=mongodb
+ - MongoDbDatabaseName=${MONGODB_DATABASE_NAME}
+ - ConnectionStrings__DefaultConnection=mongodb://${MONGODB_ROOT_USER}:${MONGODB_ROOT_PASSWORD}@telemetry-db:27017
+ depends_on:
+ telemetry-db:
+ condition: service_healthy
+
+ telemetry-db:
+ image: mongo:8.2
+ container_name: telemetry-db
+ environment:
+ - MONGO_INITDB_ROOT_USERNAME=${MONGODB_ROOT_USER}
+ - MONGO_INITDB_ROOT_PASSWORD=${MONGODB_ROOT_PASSWORD}
+ - MONGO_INITDB_DATABASE=${MONGODB_DATABASE_NAME}
+ ports:
+ - "27017:27017"
+ volumes:
+ - ./telemetry-db/mongodb:/data/db
+ healthcheck:
+ test: [ "CMD", "mongosh", "--quiet", "--eval", "'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)'" ]
+ interval: 10s
+ timeout: 5s
+ retries: 5
\ No newline at end of file
diff --git a/docker-compose.mssql.yml b/docker-compose.mssql.yml
new file mode 100644
index 0000000..0ed4022
--- /dev/null
+++ b/docker-compose.mssql.yml
@@ -0,0 +1,24 @@
+services:
+ telemetry-api:
+ environment:
+ - DbProvider=mssql
+ - ConnectionStrings__DefaultConnection=Server=telemetry-db;Database=${MSSQL_DATABASE_NAME};User Id=${MSSQL_USER};Password=${MSSQL_SA_PASSWORD};TrustServerCertificate=True;
+ depends_on:
+ telemetry-db:
+ condition: service_healthy
+
+ telemetry-db:
+ container_name: telemetry-db
+ image: mcr.microsoft.com/mssql/server:2025-latest
+ environment:
+ - ACCEPT_EULA=${MSSQL_ACCEPT_EULA}
+ - MSSQL_SA_PASSWORD=${MSSQL_SA_PASSWORD}
+ volumes:
+ - ./telemetry-db/mssql:/var/opt/mssql
+ ports:
+ - "1433:1433"
+ healthcheck:
+ test: [ "CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${MSSQL_SA_PASSWORD}' -Q 'SELECT 1' -C" ]
+ interval: 10s
+ timeout: 5s
+ retries: 5
diff --git a/docker-compose.oracle.yml b/docker-compose.oracle.yml
new file mode 100644
index 0000000..39c88ee
--- /dev/null
+++ b/docker-compose.oracle.yml
@@ -0,0 +1,28 @@
+services:
+ telemetry-api:
+ environment:
+ - DbProvider=oracle
+ - ConnectionStrings__DefaultConnection=User Id=${ORACLE_APP_USER};Password=${ORACLE_APP_USER_PASSWORD};Data Source=telemetry-db:1521/FREEPDB1;
+ depends_on:
+ telemetry-db:
+ condition: service_healthy
+
+ telemetry-db:
+ container_name: telemetry-db
+ image: gvenzl/oracle-free:23-slim
+
+ environment:
+ - ORACLE_PASSWORD=${ORACLE_PASSWORD}
+ - APP_USER=${ORACLE_APP_USER}
+ - APP_USER_PASSWORD=${ORACLE_APP_USER_PASSWORD}
+ volumes:
+ - ./telemetry-db/oracle:/opt/oracle/oradata
+ ports:
+ - "1521:1521"
+ healthcheck:
+ test: [ "CMD", "healthcheck.sh" ]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ start_period: 5s
+ start_interval: 5s
diff --git a/docker-compose.postgres.yml b/docker-compose.postgres.yml
new file mode 100644
index 0000000..b18de98
--- /dev/null
+++ b/docker-compose.postgres.yml
@@ -0,0 +1,25 @@
+services:
+ telemetry-api:
+ environment:
+ - DbProvider=postgres
+ - ConnectionStrings__DefaultConnection=Host=telemetry-db;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}
+ depends_on:
+ telemetry-db:
+ condition: service_healthy
+
+ telemetry-db:
+ container_name: telemetry-db
+ image: postgres:18-alpine
+ environment:
+ - POSTGRES_DB=${POSTGRES_DB}
+ - POSTGRES_USER=${POSTGRES_USER}
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
+ volumes:
+ - ./telemetry-db/postgres:/var/lib/postgresql
+ ports:
+ - "5432:5432"
+ healthcheck:
+ test: [ "CMD-SHELL", "pg_isready -d ${POSTGRES_DB} -U ${POSTGRES_USER}" ]
+ interval: 10s
+ timeout: 5s
+ retries: 5
diff --git a/docker-compose.sqlite.yml b/docker-compose.sqlite.yml
new file mode 100644
index 0000000..6b6e909
--- /dev/null
+++ b/docker-compose.sqlite.yml
@@ -0,0 +1,7 @@
+services:
+ telemetry-api:
+ environment:
+ - DbProvider=sqlite
+ - ConnectionStrings__DefaultConnection=Data Source=/app/data/${SQLITE_DATABASE_NAME}.db
+ volumes:
+ - ./telemetry-db/sqlite:/app/data
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..7827181
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,12 @@
+services:
+ telemetry-api:
+ container_name: telemetry-api
+ image: pyrevit-telemetry:latest
+ build:
+ context: .
+ dockerfile: src/Telemetry.Api/Dockerfile
+ ports:
+ - "8080:8080"
+ environment:
+ - ASPNETCORE_HTTP_PORTS=8080
+ - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT}
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..17d00dc
--- /dev/null
+++ b/global.json
@@ -0,0 +1,10 @@
+{
+ "sdk": {
+ "version": "10.0.0",
+ "rollForward": "latestMajor",
+ "allowPrerelease": true
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/.dockerignore b/src/Telemetry.Api/.dockerignore
new file mode 100644
index 0000000..4e2bf2a
--- /dev/null
+++ b/src/Telemetry.Api/.dockerignore
@@ -0,0 +1,28 @@
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/.idea
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+
+**/TestResults
+**/telemetry-db
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/DTOs/EngineInfoDto.cs b/src/Telemetry.Api/Application/DTOs/EngineInfoDto.cs
new file mode 100644
index 0000000..edc6938
--- /dev/null
+++ b/src/Telemetry.Api/Application/DTOs/EngineInfoDto.cs
@@ -0,0 +1,43 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+using Telemetry.Api.Domain.Constants;
+using Telemetry.Api.JsonConverters;
+
+namespace Telemetry.Api.Application.DTOs
+{
+ ///
+ /// Script engine information.
+ ///
+ public class EngineInfoDto
+ {
+ ///
+ /// Engine type name.
+ ///
Engine types.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.EngineType)]
+ public required string Type { get; init; }
+
+ ///
+ /// Engine version.
+ ///
Engines list.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.EngineVersion)]
+ public required string Version { get; init; }
+
+ ///
+ /// System paths using by script.
+ ///
+ [JsonPropertyName(PropertyNames.EngineSysPaths)]
+ public string[]? SysPath { get; init; }
+
+ ///
+ /// Dynamic script configs data.
+ ///
+ [MaxLength(8000)]
+ [JsonPropertyName(PropertyNames.EngineConfigs)]
+ [JsonConverter(typeof(DynamicDataJsonConverter))]
+ public string? Configs { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/DTOs/EventRecordDto.cs b/src/Telemetry.Api/Application/DTOs/EventRecordDto.cs
new file mode 100644
index 0000000..128d85c
--- /dev/null
+++ b/src/Telemetry.Api/Application/DTOs/EventRecordDto.cs
@@ -0,0 +1,167 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+using Telemetry.Api.Domain.Constants;
+using Telemetry.Api.JsonConverters;
+
+namespace Telemetry.Api.Application.DTOs
+{
+ ///
+ /// Event record information.
+ ///
+ public class EventRecordDto
+ {
+ ///
+ /// Unique event id.
+ ///
+ [JsonPropertyName(PropertyNames.HandlerId)]
+ public required Guid HandlerId { get; init; }
+
+ ///
+ /// Information about telemetry record.
+ ///
+ [JsonPropertyName("meta")]
+ public required MetaDto Meta { get; init; }
+
+ ///
+ /// Event type name.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.EventType)]
+ public required string EventType { get; init; }
+
+ ///
+ /// Revit event
+ /// status.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.Status)]
+ public string? Status { get; init; }
+
+ ///
+ /// When event started.
+ ///
+ [JsonPropertyName(PropertyNames.Timestamp)]
+ public required DateTimeOffset Timestamp { get; init; }
+
+ ///
+ /// Username
+ /// who use Autodesk Revit (sets in options).
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.Username)]
+ public required string Username { get; init; }
+
+ ///
+ /// Username
+ /// who logged in Windows.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.HostUsername)]
+ public string? HostUsername { get; init; }
+
+ ///
+ /// Internal
+ /// build number
+ /// of the Autodesk Revit application.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.RevitBuild)]
+ public required string RevitBuild { get; init; }
+
+ ///
+ /// Return the
+ /// primary version
+ /// of the Revit application.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.RevitVersion)]
+ public required string RevitVersion { get; init; }
+
+ ///
+ /// If event was cancelled , otherwise .
+ ///
RevitAPIEventArgs
+ /// IsCancelled method.
+ ///
+ [JsonPropertyName(PropertyNames.Cancelled)]
+ public bool? Cancelled { get; init; }
+
+ ///
+ /// If event can cancel , otherwise .
+ ///
RevitAPIEventArgs
+ /// Cancellable property.
+ ///
+ [JsonPropertyName(PropertyNames.Cancellable)]
+ public bool? Cancellable { get; init; }
+
+ ///
+ /// Id of the document that has just been closed.
+ ///
DocumentClosingEventArgs
+ /// DocumentId property.
+ ///
DocumentClosedEventArgs
+ /// DocumentId property.
+ ///
+ [JsonPropertyName(PropertyNames.DocumentId)]
+ public int DocumentId { get; init; }
+
+ ///
+ /// Type of the document, e.g. Project or Template.
+ ///
DocumentType
+ /// enumeration.
+ ///
DocumentOpeningEventArgs
+ /// DocumentType property.
+ ///
DocumentCreatingEventArgs
+ /// DocumentType property.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.DocumentType)]
+ public string? DocumentType { get; init; }
+
+ ///
+ /// Document template name.
+ ///
DocumentCreatingEventArgs
+ /// Template property.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.DocumentTemplate)]
+ public string? DocumentTemplate { get; init; }
+
+ ///
+ /// Document Title property.
+ ///
+ [MaxLength(250)]
+ [JsonPropertyName(PropertyNames.DocumentName)]
+ public string? DocumentName { get; init; }
+
+ ///
+ /// Document PathName
+ /// property.
+ ///
+ [MaxLength(1024)]
+ [JsonPropertyName(PropertyNames.DocumentPath)]
+ public string? DocumentPath { get; init; }
+
+ ///
+ /// Project name
+ /// (BuiltInParameter.PROJECT_NAME).
+ ///
+ [MaxLength(250)]
+ [JsonPropertyName(PropertyNames.ProjectName)]
+ public string? ProjectName { get; init; }
+
+ ///
+ /// Project number
+ /// (BuiltInParameter.PROJECT_NUMBER).
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.ProjectNum)]
+ public string? ProjectNum { get; init; }
+
+ ///
+ /// Dynamic event args data.
+ ///
+ [MaxLength(8000)]
+ [JsonPropertyName(PropertyNames.EventArgs)]
+ [JsonConverter(typeof(DynamicDataJsonConverter))]
+ public string? EventArgs { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/DTOs/MetaDto.cs b/src/Telemetry.Api/Application/DTOs/MetaDto.cs
new file mode 100644
index 0000000..a71058e
--- /dev/null
+++ b/src/Telemetry.Api/Application/DTOs/MetaDto.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Telemetry.Api.Application.DTOs
+{
+ ///
+ /// Record metadata.
+ ///
+ public class MetaDto
+ {
+ ///
+ /// Schema version.
+ ///
+ [JsonPropertyName("schema")]
+ public required Version SchemaVersion { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/DTOs/ScriptRecordDto.cs b/src/Telemetry.Api/Application/DTOs/ScriptRecordDto.cs
new file mode 100644
index 0000000..2b9df64
--- /dev/null
+++ b/src/Telemetry.Api/Application/DTOs/ScriptRecordDto.cs
@@ -0,0 +1,189 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+using Telemetry.Api.Domain.Constants;
+using Telemetry.Api.JsonConverters;
+
+namespace Telemetry.Api.Application.DTOs
+{
+ ///
+ /// Script record information.
+ ///
+ public class ScriptRecordDto
+ {
+ ///
+ /// Unique session id (created when revit is opened).
+ ///
+ [JsonPropertyName(PropertyNames.SessionId)]
+ public required Guid SessionId { get; init; }
+
+ ///
+ /// Information about telemetry record.
+ ///
+ [JsonPropertyName("meta")]
+ public required MetaDto Meta { get; init; }
+
+ ///
+ /// When script started.
+ ///
+ [JsonPropertyName(PropertyNames.Timestamp)]
+ public required DateTimeOffset Timestamp { get; init; }
+
+ ///
+ /// Username
+ /// who use Autodesk Revit (sets in options).
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.Username)]
+ public required string Username { get; init; }
+
+ ///
+ /// Username
+ /// who logged in Windows.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.HostUsername)]
+ public string? HostUsername { get; init; }
+
+ ///
+ /// Internal
+ /// build number
+ /// of the Autodesk Revit application.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.RevitBuild)]
+ public required string RevitBuild { get; init; }
+
+ ///
+ /// Return the
+ /// primary version
+ /// of the Revit application.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.RevitVersion)]
+ public required string RevitVersion { get; init; }
+
+ ///
+ /// pyrevit build version.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.PyRevitVersion)]
+ public required string PyRevitVersion { get; init; }
+
+ ///
+ /// pyrevit
+ /// clone name.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.CloneName)]
+ public required string CloneName { get; init; }
+
+ ///
+ /// pyrevit
+ ///
+ /// debug
+ /// mode
+ /// .
+ ///
+ [JsonPropertyName(PropertyNames.IsDebug)]
+ public bool IsDebug { get; init; }
+
+ ///
+ /// pyrevit
+ /// config mode.
+ ///
+ [JsonPropertyName(PropertyNames.IsConfig)]
+ public bool IsConfig { get; init; }
+
+ ///
+ /// If script was run from GUI (Click Revit Ribbon) , otherwise .
+ ///
+ [JsonPropertyName(PropertyNames.IsExecFromGui)]
+ public bool IsExecFromGui { get; init; }
+
+ ///
+ /// Unique execution id.
+ ///
+ [MaxLength(100)]
+ [JsonPropertyName(PropertyNames.ExecId)]
+ public required string ExecId { get; init; }
+
+ ///
+ /// When script executed.
+ ///
+ [JsonPropertyName(PropertyNames.ExecTimestamp)]
+ public required DateTimeOffset ExecTimestamp { get; init; }
+
+ ///
+ /// Command bundle name.
+ ///
+ [MaxLength(250)]
+ [JsonPropertyName(PropertyNames.CommandBundle)]
+ public required string CommandBundle { get; init; }
+
+ ///
+ /// Command extension name.
+ ///
+ [MaxLength(250)]
+ [JsonPropertyName(PropertyNames.CommandExtension)]
+ public required string CommandExtension { get; init; }
+
+ ///
+ /// Command name.
+ ///
+ [MaxLength(250)]
+ [JsonPropertyName(PropertyNames.CommandName)]
+ public required string CommandName { get; init; }
+
+ ///
+ /// Command unique name.
+ ///
+ [MaxLength(500)]
+ [JsonPropertyName(PropertyNames.CommandUniqueName)]
+ public required string CommandUniqueName { get; init; }
+
+ ///
+ /// Document Title property.
+ ///
+ [MaxLength(250)]
+ [JsonPropertyName(PropertyNames.DocumentName)]
+ public string? DocumentName { get; init; }
+
+ ///
+ /// Document PathName
+ /// property.
+ ///
+ [MaxLength(1024)]
+ [JsonPropertyName(PropertyNames.DocumentPath)]
+ public string? DocumentPath { get; init; }
+
+ ///
+ /// Script executed result code.
+ ///
ResultCode
+ /// enumeration.
+ ///
+ [JsonPropertyName(PropertyNames.ResultCode)]
+ public int ResultCode { get; init; }
+
+ ///
+ /// Executed script path.
+ ///
+ [MaxLength(1024)]
+ [JsonPropertyName(PropertyNames.ScriptPath)]
+ public required string ScriptPath { get; init; }
+
+ ///
+ /// Information about execution.
+ ///
+ [JsonPropertyName(PropertyNames.Trace)]
+ public required TraceInfoDto Trace { get; init; }
+
+ ///
+ /// Additional command results.
+ ///
+ [MaxLength(8000)]
+ [JsonPropertyName(PropertyNames.CommandResults)]
+ [JsonConverter(typeof(DynamicDataJsonConverter))]
+ public string? CommandResults { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/DTOs/StatusRecordDto.cs b/src/Telemetry.Api/Application/DTOs/StatusRecordDto.cs
new file mode 100644
index 0000000..43241a3
--- /dev/null
+++ b/src/Telemetry.Api/Application/DTOs/StatusRecordDto.cs
@@ -0,0 +1,52 @@
+using System.Text.Json.Serialization;
+
+namespace Telemetry.Api.Application.DTOs
+{
+ ///
+ /// Server status information.
+ ///
+ public class StatusRecordDto
+ {
+ ///
+ /// Connection status (pass or fail).
+ ///
+ [JsonPropertyName("status")]
+ public required string Status { get; init; }
+
+ ///
+ /// Service unique id.
+ ///
+ [JsonPropertyName("serviceid")]
+ public required Guid ServiceId { get; init; }
+
+ ///
+ /// Service version.
+ ///
+ [JsonPropertyName("version")]
+ public required string Version { get; init; }
+
+ ///
+ /// Check information.
+ ///
+ [JsonPropertyName("checks")]
+ public required Dictionary Checks { get; init; }
+ }
+
+ ///
+ /// Data transfer object representing the status check details.
+ ///
+ public class StatusCheckDto
+ {
+ ///
+ /// Data base connection status (pass or fail).
+ ///
+ [JsonPropertyName("status")]
+ public required string Status { get; init; }
+
+ ///
+ /// Data base version.
+ ///
+ [JsonPropertyName("version")]
+ public required string Version { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/DTOs/TraceInfoDto.cs b/src/Telemetry.Api/Application/DTOs/TraceInfoDto.cs
new file mode 100644
index 0000000..eebeb6f
--- /dev/null
+++ b/src/Telemetry.Api/Application/DTOs/TraceInfoDto.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+using Telemetry.Api.Domain.Constants;
+
+namespace Telemetry.Api.Application.DTOs
+{
+ ///
+ /// Script executed information.
+ ///
+ public class TraceInfoDto
+ {
+ ///
+ /// Script executed
+ /// message.
+ ///
+ [MaxLength(8000)]
+ [JsonPropertyName(PropertyNames.TraceMessage)]
+ public required string Message { get; init; }
+
+ ///
+ /// Script engine information.
+ ///
+ [JsonPropertyName(PropertyNames.Engine)]
+ public required EngineInfoDto Engine { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/Interfaces/IApplicationDbContext.cs b/src/Telemetry.Api/Application/Interfaces/IApplicationDbContext.cs
new file mode 100644
index 0000000..3a00103
--- /dev/null
+++ b/src/Telemetry.Api/Application/Interfaces/IApplicationDbContext.cs
@@ -0,0 +1,51 @@
+using Microsoft.EntityFrameworkCore;
+using Telemetry.Api.Domain.Models;
+
+namespace Telemetry.Api.Application.Interfaces
+{
+ ///
+ /// Represents the contract for the application's database context.
+ /// Defines the operations and entities available for data access in the application.
+ ///
+ public interface IApplicationDbContext
+ {
+ ///
+ /// Asynchronously adds a new event record to the database context.
+ ///
+ /// A task that represents the asynchronous add operation.
+ Task AddEventRecord(EventRecord eventRecord, CancellationToken cancellationToken);
+
+ ///
+ /// Asynchronously adds a new script record to the database context.
+ ///
+ /// A task that represents the asynchronous add operation.
+ Task AddScriptRecord(ScriptRecord scriptRecord, CancellationToken cancellationToken);
+
+ ///
+ /// Asynchronously saves all changes made in the current context to the database.
+ ///
+ /// A task that represents the asynchronous save operation.
+ /// The task result contains the number of state entries written to the database.
+ Task SaveChangesAsync(CancellationToken cancellationToken);
+
+ ///
+ /// Asynchronously checks if a connection to the database can be established.
+ ///
+ /// A task that represents the asynchronous operation.
+ /// The task result contains a boolean value indicating whether the connection attempt succeeded.
+ Task CanConnectAsync(CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the database provider being used by the application.
+ ///
+ /// A string representing the name of the database provider.
+ string GetDbProvider();
+
+ ///
+ /// Asynchronously retrieves the database version.
+ ///
+ /// A task that represents the asynchronous operation.
+ /// The task result contains the version string of the database.
+ Task GetDbVersionAsync(CancellationToken cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/Interfaces/IServiceInfo.cs b/src/Telemetry.Api/Application/Interfaces/IServiceInfo.cs
new file mode 100644
index 0000000..739d765
--- /dev/null
+++ b/src/Telemetry.Api/Application/Interfaces/IServiceInfo.cs
@@ -0,0 +1,20 @@
+namespace Telemetry.Api.Application.Interfaces
+{
+ ///
+ /// Represents a service information interface that provides details
+ /// or metadata about a specific service.
+ ///
+ public interface IServiceInfo
+ {
+ ///
+ /// Gets or sets the unique identifier for the service.
+ ///
+ ///
+ /// The ServiceId property is used to uniquely identify a specific service
+ /// within the system. This value is typically generated or assigned
+ /// during service creation and should remain constant to ensure
+ /// consistent identification.
+ ///
+ Guid ServiceId { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/Mappings/TelemetryMappingExtensions.cs b/src/Telemetry.Api/Application/Mappings/TelemetryMappingExtensions.cs
new file mode 100644
index 0000000..808d62e
--- /dev/null
+++ b/src/Telemetry.Api/Application/Mappings/TelemetryMappingExtensions.cs
@@ -0,0 +1,212 @@
+using Telemetry.Api.Application.DTOs;
+using Telemetry.Api.Domain.Models;
+
+namespace Telemetry.Api.Application.Mappings
+{
+ ///
+ /// Provides extension methods for mapping telemetry-related data.
+ ///
+ public static class TelemetryMappingExtensions
+ {
+ ///
+ /// Converts the specified source object to a model representation.
+ ///
+ /// The source object to be converted.
+ /// A new instance of the model object created from the source.
+ public static MetaRecord ToModel(this MetaDto dto)
+ {
+ return new MetaRecord {SchemaVersion = (Version)dto.SchemaVersion.Clone()};
+ }
+
+ ///
+ /// Converts the specified source object to a data transfer object (DTO).
+ ///
+ /// The source object to be converted.
+ /// A new instance of the data transfer object (DTO) created from the source.
+ public static MetaDto ToDto(this MetaRecord model)
+ {
+
+ return new MetaDto {SchemaVersion = (Version)model.SchemaVersion.Clone()};
+ }
+
+ ///
+ /// Transforms the input data transfer object into its corresponding model representation.
+ ///
+ /// The input data transfer object to be transformed.
+ /// An instance of the model created from the provided data transfer object.
+ public static EngineInfo ToModel(this EngineInfoDto dto)
+ {
+ return new EngineInfo
+ {
+ Type = dto.Type, Version = dto.Version, SysPaths = dto.SysPath, Configs = dto.Configs
+ };
+ }
+
+ ///
+ /// Converts the specified source model into a Data Transfer Object (DTO) representation.
+ ///
+ /// The source model to be converted.
+ /// A new DTO instance created from the provided model.
+ public static EngineInfoDto ToDto(this EngineInfo model)
+ {
+ return new EngineInfoDto
+ {
+ Type = model.Type, Version = model.Version, SysPath = model.SysPaths, Configs = model.Configs
+ };
+ }
+
+ ///
+ /// Transforms the provided data transfer object (DTO) into its corresponding model representation.
+ ///
+ /// The data transfer object to be transformed.
+ /// A model instance derived from the given data transfer object.
+ public static TraceInfo ToModel(this TraceInfoDto dto)
+ {
+ return new TraceInfo {Message = dto.Message, Engine = dto.Engine.ToModel()};
+ }
+
+ ///
+ /// Converts the specified model object to a data transfer object (DTO) representation.
+ ///
+ /// The model object to be converted.
+ /// A new instance of the DTO created from the model.
+ public static TraceInfoDto ToDto(this TraceInfo model)
+ {
+ return new TraceInfoDto {Message = model.Message, Engine = model.Engine.ToDto()};
+ }
+
+ ///
+ /// Transforms the provided data transfer object into its corresponding model representation.
+ ///
+ /// The data transfer object to be transformed.
+ /// The model object derived from the given data transfer object.
+ public static ScriptRecord ToModel(this ScriptRecordDto dto)
+ {
+ return new ScriptRecord
+ {
+ Id = Guid.CreateVersion7(),
+ SessionId = dto.SessionId,
+ Timestamp = dto.Timestamp,
+ Username = dto.Username,
+ HostUsername = dto.HostUsername,
+ RevitBuild = dto.RevitBuild,
+ RevitVersion = dto.RevitVersion,
+ PyRevitVersion = dto.PyRevitVersion,
+ CloneName = dto.CloneName,
+ IsDebug = dto.IsDebug,
+ IsConfig = dto.IsConfig,
+ IsExecFromGui = dto.IsExecFromGui,
+ ExecId = dto.ExecId,
+ ExecTimestamp = dto.ExecTimestamp,
+ CommandBundle = dto.CommandBundle,
+ CommandExtension = dto.CommandExtension,
+ CommandName = dto.CommandName,
+ CommandUniqueName = dto.CommandUniqueName,
+ DocumentName = dto.DocumentName,
+ DocumentPath = dto.DocumentPath,
+ ResultCode = dto.ResultCode,
+ ScriptPath = dto.ScriptPath,
+ Trace = dto.Trace.ToModel(),
+ CommandResults = dto.CommandResults
+ };
+ }
+
+ ///
+ /// Converts the specified source object to a data transfer object representation.
+ ///
+ /// The source object to be converted.
+ /// A new instance of the data transfer object created from the source.
+ public static ScriptRecordDto ToDto(this ScriptRecord model)
+ {
+ return new ScriptRecordDto
+ {
+ SessionId = model.SessionId,
+ Meta = new MetaDto(){SchemaVersion = new Version(2,0,0)},
+ Timestamp = model.Timestamp,
+ Username = model.Username,
+ HostUsername = model.HostUsername,
+ RevitBuild = model.RevitBuild,
+ RevitVersion = model.RevitVersion,
+ PyRevitVersion = model.PyRevitVersion,
+ CloneName = model.CloneName,
+ IsDebug = model.IsDebug,
+ IsConfig = model.IsConfig,
+ IsExecFromGui = model.IsExecFromGui,
+ ExecId = model.ExecId,
+ ExecTimestamp = model.ExecTimestamp,
+ CommandBundle = model.CommandBundle,
+ CommandExtension = model.CommandExtension,
+ CommandName = model.CommandName,
+ CommandUniqueName = model.CommandUniqueName,
+ DocumentName = model.DocumentName,
+ DocumentPath = model.DocumentPath,
+ ResultCode = model.ResultCode,
+ ScriptPath = model.ScriptPath,
+ Trace = model.Trace.ToDto(),
+ CommandResults = model.CommandResults
+ };
+ }
+
+ ///
+ /// Transforms the provided DTO into its corresponding model representation.
+ ///
+ /// The data transfer object to be transformed.
+ /// An instance of the model derived from the given DTO.
+ public static EventRecord ToModel(this EventRecordDto dto)
+ {
+ return new EventRecord
+ {
+ Id = Guid.CreateVersion7(),
+ HandlerId = dto.HandlerId,
+ EventType = dto.EventType,
+ Status = dto.Status,
+ Timestamp = dto.Timestamp,
+ Username = dto.Username,
+ HostUsername = dto.HostUsername,
+ RevitBuild = dto.RevitBuild,
+ RevitVersion = dto.RevitVersion,
+ Cancelled = dto.Cancelled,
+ Cancellable = dto.Cancellable,
+ DocumentId = dto.DocumentId,
+ DocumentType = dto.DocumentType,
+ DocumentTemplate = dto.DocumentTemplate,
+ DocumentName = dto.DocumentName,
+ DocumentPath = dto.DocumentPath,
+ ProjectName = dto.ProjectName,
+ ProjectNum = dto.ProjectNum,
+ EventArgs = dto.EventArgs
+ };
+ }
+
+ ///
+ /// Converts the specified model object to a data transfer object representation.
+ ///
+ /// The model object to be converted.
+ /// A new instance of the data transfer object created from the model.
+ public static EventRecordDto ToDto(this EventRecord model)
+ {
+ return new EventRecordDto
+ {
+ HandlerId = model.HandlerId,
+ Meta = new MetaDto(){SchemaVersion = new Version(2,0,0)},
+ EventType = model.EventType,
+ Status = model.Status,
+ Timestamp = model.Timestamp,
+ Username = model.Username,
+ HostUsername = model.HostUsername,
+ RevitBuild = model.RevitBuild,
+ RevitVersion = model.RevitVersion,
+ Cancelled = model.Cancelled,
+ Cancellable = model.Cancellable,
+ DocumentId = model.DocumentId,
+ DocumentType = model.DocumentType,
+ DocumentTemplate = model.DocumentTemplate,
+ DocumentName = model.DocumentName,
+ DocumentPath = model.DocumentPath,
+ ProjectName = model.ProjectName,
+ ProjectNum = model.ProjectNum,
+ EventArgs = model.EventArgs
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Application/Services/ServiceInfo.cs b/src/Telemetry.Api/Application/Services/ServiceInfo.cs
new file mode 100644
index 0000000..87a2e33
--- /dev/null
+++ b/src/Telemetry.Api/Application/Services/ServiceInfo.cs
@@ -0,0 +1,29 @@
+using Telemetry.Api.Application.Interfaces;
+
+namespace Telemetry.Api.Application.Services
+{
+ ///
+ /// Represents information about a service, encapsulating details
+ /// such as the service name, description, status, and other relevant metadata.
+ ///
+ public class ServiceInfo : IServiceInfo
+ {
+ ///
+ /// Represents information about a service, including its name,
+ /// description, and any related metadata.
+ ///
+ public ServiceInfo()
+ {
+ ServiceId = Guid.NewGuid();
+ }
+
+ ///
+ /// Gets or sets the unique identifier for the service.
+ ///
+ ///
+ /// The ServiceId property is used to uniquely identify a specific service within the system.
+ /// This identifier is typically assigned automatically and is immutable once set.
+ ///
+ public Guid ServiceId { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Dockerfile b/src/Telemetry.Api/Dockerfile
new file mode 100644
index 0000000..bb6eb9c
--- /dev/null
+++ b/src/Telemetry.Api/Dockerfile
@@ -0,0 +1,57 @@
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+
+# fix cannot load library libgssapi_krb5.so.2 (postgres)
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends libgssapi-krb5-2 \
+ && rm -rf /var/lib/apt/lists/*
+
+USER $APP_UID
+WORKDIR /app
+
+EXPOSE 8080
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+
+COPY ["Directory.Build.props", "Directory.Build.props"]
+COPY ["Directory.Build.targets", "Directory.Build.targets"]
+COPY ["Directory.Packages.props", "Directory.Packages.props"]
+
+COPY ["src/Telemetry.Api/Telemetry.Api.csproj", "Telemetry.Api/"]
+COPY ["src/Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj", "Telemetry.Migrations.Oracle/"]
+COPY ["src/Telemetry.Migrations.Postgres/Telemetry.Migrations.Postgres.csproj", "Telemetry.Migrations.Postgres/"]
+COPY ["src/Telemetry.Migrations.Sqlite/Telemetry.Migrations.Sqlite.csproj", "Telemetry.Migrations.Sqlite/"]
+COPY ["src/Telemetry.Migrations.SqlServer/Telemetry.Migrations.SqlServer.csproj", "Telemetry.Migrations.SqlServer/"]
+
+RUN dotnet restore "./Telemetry.Api/Telemetry.Api.csproj"
+RUN dotnet restore "./Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj"
+RUN dotnet restore "./Telemetry.Migrations.Postgres/Telemetry.Migrations.Postgres.csproj"
+RUN dotnet restore "./Telemetry.Migrations.Sqlite/Telemetry.Migrations.Sqlite.csproj"
+RUN dotnet restore "./Telemetry.Migrations.SqlServer/Telemetry.Migrations.SqlServer.csproj"
+
+COPY . .
+RUN dotnet build "./src/Telemetry.Api/Telemetry.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
+RUN dotnet build "./src/Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj" -c $BUILD_CONFIGURATION -o /app/build
+RUN dotnet build "./src/Telemetry.Migrations.Postgres/Telemetry.Migrations.Postgres.csproj" -c $BUILD_CONFIGURATION -o /app/build
+RUN dotnet build "./src/Telemetry.Migrations.Sqlite/Telemetry.Migrations.Sqlite.csproj" -c $BUILD_CONFIGURATION -o /app/build
+RUN dotnet build "./src/Telemetry.Migrations.SqlServer/Telemetry.Migrations.SqlServer.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./src/Telemetry.Api/Telemetry.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+RUN dotnet publish "./src/Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj" -c $BUILD_CONFIGURATION -o /app/migrations/oracle
+RUN dotnet publish "./src/Telemetry.Migrations.Postgres/Telemetry.Migrations.Postgres.csproj" -c $BUILD_CONFIGURATION -o /app/migrations/postgres
+RUN dotnet publish "./src/Telemetry.Migrations.Sqlite/Telemetry.Migrations.Sqlite.csproj" -c $BUILD_CONFIGURATION -o /app/migrations/sqlite
+RUN dotnet publish "./src/Telemetry.Migrations.SqlServer/Telemetry.Migrations.SqlServer.csproj" -c $BUILD_CONFIGURATION -o /app/migrations/sqlserver
+
+FROM base AS final
+WORKDIR /app
+
+COPY --from=publish /app/publish .
+COPY --from=publish /app/migrations/oracle/Telemetry.Migrations.Oracle.dll .
+COPY --from=publish /app/migrations/postgres/Telemetry.Migrations.Postgres.dll .
+COPY --from=publish /app/migrations/sqlite/Telemetry.Migrations.Sqlite.dll .
+COPY --from=publish /app/migrations/sqlserver/Telemetry.Migrations.SqlServer.dll .
+
+ENTRYPOINT ["dotnet", "Telemetry.Api.dll"]
diff --git a/src/Telemetry.Api/Domain/Constants/PropertyNames.cs b/src/Telemetry.Api/Domain/Constants/PropertyNames.cs
new file mode 100644
index 0000000..e9ccb31
--- /dev/null
+++ b/src/Telemetry.Api/Domain/Constants/PropertyNames.cs
@@ -0,0 +1,131 @@
+namespace Telemetry.Api.Domain.Constants
+{
+ ///
+ /// Property names constants.
+ ///
+ public static class PropertyNames
+ {
+ /// Unique identifier.
+ public const string Id = "_id";
+
+ /// Session identifier.
+ public const string SessionId = "sessionid";
+
+ /// Timestamp.
+ public const string Timestamp = "timestamp";
+
+ /// Username.
+ public const string Username = "username";
+
+ /// Host username.
+ public const string HostUsername = "host_user";
+
+ /// Revit build version.
+ public const string RevitBuild = "revitbuild";
+
+ /// Revit version.
+ public const string RevitVersion = "revit";
+
+ /// pyRevit version.
+ public const string PyRevitVersion = "pyrevit";
+
+ /// Clone name.
+ public const string CloneName = "clone";
+
+ /// Is debug mode.
+ public const string IsDebug = "debug";
+
+ /// Is config mode.
+ public const string IsConfig = "config";
+
+ /// Is executed from GUI.
+ public const string IsExecFromGui = "from_gui";
+
+ /// Execution identifier.
+ public const string ExecId = "exec_id";
+
+ /// Execution timestamp.
+ public const string ExecTimestamp = "exec_timestamp";
+
+ /// Command bundle.
+ public const string CommandBundle = "commandbundle";
+
+ /// Command extension.
+ public const string CommandExtension = "commandextension";
+
+ /// Command name.
+ public const string CommandName = "commandname";
+
+ /// Command unique name.
+ public const string CommandUniqueName = "commanduniquename";
+
+ /// Document name.
+ public const string DocumentName = "docname";
+
+ /// Document path.
+ public const string DocumentPath = "docpath";
+
+ /// Result code.
+ public const string ResultCode = "resultcode";
+
+ /// Script path.
+ public const string ScriptPath = "scriptpath";
+
+ /// Command results.
+ public const string CommandResults = "commandresults";
+
+ /// Trace information.
+ public const string Trace = "trace";
+
+ /// Trace message.
+ public const string TraceMessage = "message";
+
+ /// Engine information.
+ public const string Engine = "engine";
+
+ /// Engine type.
+ public const string EngineType = "type";
+
+ /// Engine version.
+ public const string EngineVersion = "version";
+
+ /// Engine configs.
+ public const string EngineConfigs = "configs";
+
+ /// Engine system paths.
+ public const string EngineSysPaths = "syspath";
+
+ /// Handler identifier.
+ public const string HandlerId = "handler_id";
+
+ /// Event type.
+ public const string EventType = "type";
+
+ /// Status.
+ public const string Status = "status";
+
+ /// Is cancelled.
+ public const string Cancelled = "cancelled";
+
+ /// Is cancellable.
+ public const string Cancellable = "cancellable";
+
+ /// Document identifier.
+ public const string DocumentId = "docid";
+
+ /// Document type.
+ public const string DocumentType = "doctype";
+
+ /// Document template.
+ public const string DocumentTemplate = "doctemplate";
+
+ /// Project name.
+ public const string ProjectName = "projectname";
+
+ /// Project number.
+ public const string ProjectNum = "projectnum";
+
+ /// Event arguments.
+ public const string EventArgs = "args";
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Domain/Models/EngineInfo.cs b/src/Telemetry.Api/Domain/Models/EngineInfo.cs
new file mode 100644
index 0000000..d2b9e73
--- /dev/null
+++ b/src/Telemetry.Api/Domain/Models/EngineInfo.cs
@@ -0,0 +1,43 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+using Telemetry.Api.Domain.Constants;
+using Telemetry.Api.JsonConverters;
+
+namespace Telemetry.Api.Domain.Models
+{
+ ///
+ /// Script engine information.
+ ///
+ public class EngineInfo
+ {
+ ///
+ /// Engine type name.
+ ///
Engine types.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.EngineType)]
+ public required string Type { get; init; }
+
+ ///
+ /// Engine version.
+ ///
Engines list.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.EngineVersion)]
+ public required string Version { get; init; }
+
+ ///
+ /// System paths using by script.
+ ///
+ [Column(PropertyNames.EngineSysPaths)]
+ public string[]? SysPaths { get; init; }
+
+ ///
+ /// Dynamic script configs data.
+ ///
+ [MaxLength(8000)]
+ [Column(PropertyNames.EngineConfigs)]
+ public string? Configs { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Domain/Models/EventRecord.cs b/src/Telemetry.Api/Domain/Models/EventRecord.cs
new file mode 100644
index 0000000..bc6348f
--- /dev/null
+++ b/src/Telemetry.Api/Domain/Models/EventRecord.cs
@@ -0,0 +1,169 @@
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Telemetry.Api.Domain.Constants;
+
+namespace Telemetry.Api.Domain.Models
+{
+ ///
+ /// Event record information.
+ ///
+ [Table("events")]
+ public class EventRecord
+ {
+ ///
+ /// Event id.
+ ///
+ [Key]
+ [Column(PropertyNames.Id)]
+ public Guid Id { get; init; }
+
+ ///
+ /// Unique event id.
+ ///
+ [Column(PropertyNames.HandlerId)]
+ public Guid HandlerId { get; init; }
+
+ ///
+ /// Event type name.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.EventType)]
+ public required string EventType { get; init; }
+
+ ///
+ /// Revit event
+ /// status.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.Status)]
+ public string? Status { get; init; }
+
+ ///
+ /// When event started.
+ ///
+ [Column(PropertyNames.Timestamp)]
+ public DateTimeOffset Timestamp { get; init; }
+
+ ///
+ /// Username
+ /// who use Autodesk Revit (sets in options).
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.Username)]
+ public required string Username { get; init; }
+
+ ///
+ /// Username
+ /// who logged in Windows.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.HostUsername)]
+ public string? HostUsername { get; init; }
+
+ ///
+ /// Internal
+ /// build number
+ /// of the Autodesk Revit application.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.RevitBuild)]
+ public required string RevitBuild { get; init; }
+
+ ///
+ /// Return the
+ /// primary version
+ /// of the Revit application.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.RevitVersion)]
+ public required string RevitVersion { get; init; }
+
+ ///
+ /// If event was cancelled , otherwise .
+ ///
RevitAPIEventArgs
+ /// IsCancelled method.
+ ///
+ [Column(PropertyNames.Cancelled)]
+ public bool? Cancelled { get; init; }
+
+ ///
+ /// If event can cancel , otherwise .
+ ///
RevitAPIEventArgs
+ /// Cancellable property.
+ ///
+ [Column(PropertyNames.Cancellable)]
+ public bool? Cancellable { get; init; }
+
+ ///
+ /// Id of the document that has just been closed.
+ ///
DocumentClosingEventArgs
+ /// DocumentId property.
+ ///
DocumentClosedEventArgs
+ /// DocumentId property.
+ ///
+ [Column(PropertyNames.DocumentId)]
+ public int DocumentId { get; init; }
+
+ ///
+ /// Type of the document, e.g. Project or Template.
+ ///
DocumentType
+ /// enumeration.
+ ///
DocumentOpeningEventArgs
+ /// DocumentType property.
+ ///
DocumentCreatingEventArgs
+ /// DocumentType property.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.DocumentType)]
+ public string? DocumentType { get; init; }
+
+ ///
+ /// Document template name.
+ ///
DocumentCreatingEventArgs
+ /// Template property.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.DocumentTemplate)]
+ public string? DocumentTemplate { get; init; }
+
+ ///
+ /// Document Title property.
+ ///
+ [MaxLength(250)]
+ [Column(PropertyNames.DocumentName)]
+ public string? DocumentName { get; init; }
+
+ ///
+ /// Document PathName
+ /// property.
+ ///
+ [MaxLength(1024)]
+ [Column(PropertyNames.DocumentPath)]
+ public string? DocumentPath { get; init; }
+
+ ///
+ /// Project name
+ /// (BuiltInParameter.PROJECT_NAME).
+ ///
+ [MaxLength(250)]
+ [Column(PropertyNames.ProjectName)]
+ public string? ProjectName { get; init; }
+
+ ///
+ /// Project number
+ /// (BuiltInParameter.PROJECT_NUMBER).
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.ProjectNum)]
+ public string? ProjectNum { get; init; }
+
+ ///
+ /// Dynamic event args data.
+ ///
+ [MaxLength(8000)]
+ [Column(PropertyNames.EventArgs)]
+ public string? EventArgs { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Domain/Models/MetaRecord.cs b/src/Telemetry.Api/Domain/Models/MetaRecord.cs
new file mode 100644
index 0000000..687de6b
--- /dev/null
+++ b/src/Telemetry.Api/Domain/Models/MetaRecord.cs
@@ -0,0 +1,17 @@
+using MongoDB.Bson.Serialization.Attributes;
+using System.Text.Json.Serialization;
+
+namespace Telemetry.Api.Domain.Models
+{
+ ///
+ /// Record metadata.
+ ///
+ public class MetaRecord
+ {
+ ///
+ /// Schema version.
+ ///
+ [JsonPropertyName("version")]
+ public required Version SchemaVersion { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Domain/Models/ScriptRecord.cs b/src/Telemetry.Api/Domain/Models/ScriptRecord.cs
new file mode 100644
index 0000000..55d8995
--- /dev/null
+++ b/src/Telemetry.Api/Domain/Models/ScriptRecord.cs
@@ -0,0 +1,193 @@
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+using Telemetry.Api.Domain.Constants;
+using Telemetry.Api.JsonConverters;
+
+namespace Telemetry.Api.Domain.Models
+{
+ ///
+ /// Script record information.
+ ///
+ [Table("scripts")]
+ public class ScriptRecord
+ {
+ ///
+ /// Scripts id.
+ ///
+ [Key]
+ [Column(PropertyNames.Id)]
+ public Guid Id { get; init; }
+
+ ///
+ /// Unique session id (created when revit is opened).
+ ///
+ [Column(PropertyNames.SessionId)]
+ public Guid SessionId { get; init; }
+
+ ///
+ /// When script started.
+ ///
+ [Column(PropertyNames.Timestamp)]
+ public DateTimeOffset Timestamp { get; init; }
+
+ ///
+ /// Username
+ /// who use Autodesk Revit (sets in options).
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.Username)]
+ public required string Username { get; init; }
+
+ ///
+ /// Username
+ /// who logged in Windows.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.HostUsername)]
+ public string? HostUsername { get; init; }
+
+ ///
+ /// Internal
+ /// build number
+ /// of the Autodesk Revit application.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.RevitBuild)]
+ public required string RevitBuild { get; init; }
+
+ ///
+ /// Return the
+ /// primary version
+ /// of the Revit application.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.RevitVersion)]
+ public required string RevitVersion { get; init; }
+
+ ///
+ /// pyrevit build version.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.PyRevitVersion)]
+ public required string PyRevitVersion { get; init; }
+
+ ///
+ /// pyrevit
+ /// clone name.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.CloneName)]
+ public required string CloneName { get; init; }
+
+ ///
+ /// pyrevit
+ ///
+ /// debug
+ /// mode
+ /// .
+ ///
+ [Column(PropertyNames.IsDebug)]
+ public bool IsDebug { get; init; }
+
+ ///
+ /// pyrevit
+ /// config mode.
+ ///
+ [Column(PropertyNames.IsConfig)]
+ public bool IsConfig { get; init; }
+
+ ///
+ /// If script was run from GUI (Click Revit Ribbon) , otherwise .
+ ///
+ [Column(PropertyNames.IsExecFromGui)]
+ public bool IsExecFromGui { get; init; }
+
+ ///
+ /// Unique execution id.
+ ///
+ [MaxLength(100)]
+ [Column(PropertyNames.ExecId)]
+ public required string ExecId { get; init; }
+
+ ///
+ /// When script executed.
+ ///
+ [Column(PropertyNames.ExecTimestamp)]
+ public DateTimeOffset ExecTimestamp { get; init; }
+
+ ///
+ /// Command bundle name.
+ ///
+ [MaxLength(250)]
+ [Column(PropertyNames.CommandBundle)]
+ public required string CommandBundle { get; init; }
+
+ ///
+ /// Command extension name.
+ ///
+ [MaxLength(250)]
+ [Column(PropertyNames.CommandExtension)]
+ public required string CommandExtension { get; init; }
+
+ ///
+ /// Command name.
+ ///
+ [MaxLength(250)]
+ [Column(PropertyNames.CommandName)]
+ public required string CommandName { get; init; }
+
+ ///
+ /// Command unique name.
+ ///
+ [MaxLength(500)]
+ [Column(PropertyNames.CommandUniqueName)]
+ public required string CommandUniqueName { get; init; }
+
+ ///
+ /// Document Title property.
+ ///
+ [MaxLength(250)]
+ [Column(PropertyNames.DocumentName)]
+ public string? DocumentName { get; init; }
+
+ ///
+ /// Document PathName
+ /// property.
+ ///
+ [MaxLength(1024)]
+ [Column(PropertyNames.DocumentPath)]
+ public string? DocumentPath { get; init; }
+
+ ///
+ /// Script executed result code.
+ ///
ResultCode
+ /// enumeration.
+ ///
+ [Column(PropertyNames.ResultCode)]
+ public int ResultCode { get; init; }
+
+ ///
+ /// Executed script path.
+ ///
+ [MaxLength(1024)]
+ [Column(PropertyNames.ScriptPath)]
+ public required string ScriptPath { get; init; }
+
+ ///
+ /// Information about execution.
+ ///
+ [Column(PropertyNames.Trace)]
+ public required TraceInfo Trace { get; init; }
+
+ ///
+ /// Additional command results.
+ ///
+ [MaxLength(8000)]
+ [Column(PropertyNames.CommandResults)]
+ public string? CommandResults { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Domain/Models/TraceInfo.cs b/src/Telemetry.Api/Domain/Models/TraceInfo.cs
new file mode 100644
index 0000000..f2b7d88
--- /dev/null
+++ b/src/Telemetry.Api/Domain/Models/TraceInfo.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+using Telemetry.Api.Domain.Constants;
+
+namespace Telemetry.Api.Domain.Models
+{
+ ///
+ /// Script executed information.
+ ///
+ public class TraceInfo
+ {
+ ///
+ /// Script executed
+ /// message.
+ ///
+ [MaxLength(8000)]
+ [Column(PropertyNames.TraceMessage)]
+ public required string Message { get; init; }
+
+ ///
+ /// Script engine information.
+ ///
+ [Column(PropertyNames.Engine)]
+ public required EngineInfo Engine { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Telemetry.Api/Infrastructure/Persistence/ApplicationDbContext.cs
new file mode 100644
index 0000000..3b0f632
--- /dev/null
+++ b/src/Telemetry.Api/Infrastructure/Persistence/ApplicationDbContext.cs
@@ -0,0 +1,223 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using MongoDB.Bson;
+using MongoDB.EntityFrameworkCore.Extensions;
+using System.Data;
+using System.Data.Common;
+using System.Reflection;
+using System.Text.Json;
+using Telemetry.Api.Application.Interfaces;
+using Telemetry.Api.Domain.Constants;
+using Telemetry.Api.Domain.Models;
+
+namespace Telemetry.Api.Infrastructure.Persistence
+{
+ ///
+ /// Represents the database context for the application, providing access to the database
+ /// and handling the interactions between the application and the data storage.
+ ///
+ ///
+ /// This class is typically used with Entity Framework Core to define the database schema
+ /// through DbSet properties and manage entity persistence.
+ /// It encapsulates the data connection and performs CRUD operations on the underlying database.
+ ///
+ ///
+ /// The ApplicationDbContext class should be instantiated and managed using dependency
+ /// injection within the application's service container. Configure the DbContext options
+ /// in the application startup or configuration class.
+ ///
+ public class ApplicationDbContext : DbContext, IApplicationDbContext
+ {
+ ///
+ /// Represents the database context for the application.
+ /// Provides access to the database tables and enables CRUD operations.
+ /// Extends from DbContext to leverage EF Core functionality for managing database interactions.
+ ///
+ public ApplicationDbContext(DbContextOptions options) : base(options) { }
+
+ ///
+ /// Gets or sets the collection of event records associated with the current context.
+ ///
+ ///
+ /// This property holds a list of event data used for logging, auditing, or tracking purposes.
+ /// It can be used to store and retrieve information about events in a structured format.
+ ///
+ public DbSet EventRecords => Set();
+
+ ///
+ /// Gets or sets the collection of script records associated with the current context.
+ ///
+ ///
+ /// This property contains a list of script data, which can be used for processing,
+ /// execution tracking, or storing metadata related to scripts.
+ ///
+ public DbSet ScriptRecords => Set();
+
+ ///
+ /// Asynchronously adds a new event record to the database context.
+ ///
+ /// A task that represents the asynchronous add operation.
+ public async Task AddEventRecord(EventRecord eventRecord, CancellationToken cancellationToken)
+ {
+ await EventRecords.AddAsync(eventRecord, cancellationToken);
+ }
+
+ ///
+ /// Asynchronously adds a new script record to the database context.
+ ///
+ /// A task that represents the asynchronous add operation.
+ public async Task AddScriptRecord(ScriptRecord scriptRecord, CancellationToken cancellationToken)
+ {
+ await ScriptRecords.AddAsync(scriptRecord, cancellationToken);
+ }
+
+ ///
+ /// Asynchronously checks whether a successful connection to the database can be established.
+ /// This method verifies connectivity without performing any data manipulation or transactions.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a boolean value indicating
+ /// whether the connection to the database was successful (true) or unsuccessful (false).
+ ///
+ public Task CanConnectAsync(CancellationToken cancellationToken)
+ {
+ return Database.CanConnectAsync(cancellationToken);
+ }
+
+ ///
+ /// Retrieves the database provider being used for the application's data access.
+ /// This method determines and returns the current database provider implementation.
+ ///
+ /// The database provider instance configured for the application.
+ public string GetDbProvider()
+ {
+ return Database.ProviderName ?? "unknown";
+ }
+
+ ///
+ /// Asynchronously retrieves the current version of the database.
+ /// Useful for determining database schema versions or migrations required.
+ ///
+ /// A token to monitor for cancellation requests.
+ /// A task representing the asynchronous operation. The task result contains the database version as a string.
+ public async Task GetDbVersionAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ string? query = null;
+ if (Database.IsNpgsql())
+ {
+ query = "SELECT version()";
+ }
+ else if (Database.IsOracle())
+ {
+ query = "SELECT * FROM v$version;";
+ }
+ else if (Database.IsSqlServer())
+ {
+ query = "SELECT @@VERSION";
+ }
+ else if (Database.IsSqlite())
+ {
+ query = "SELECT sqlite_version()";
+ }
+ else if (Database.IsInMemory())
+ {
+ return "memorydb";
+ }
+ else if (Database.ProviderName!.Equals("MongoDB.EntityFrameworkCore"))
+ {
+ return $"EF Core lib version: " +
+ $"{typeof(MongoPropertyBuilderExtensions).Assembly.GetName().Version?.ToString() ?? "mongodb"}";
+ }
+
+ if (query != null)
+ {
+ await using DbCommand command = Database.GetDbConnection().CreateCommand();
+
+ command.CommandText = query;
+ if (command.Connection!.State != ConnectionState.Open)
+ {
+ await command.Connection.OpenAsync(cancellationToken);
+ }
+
+ object? result = await command.ExecuteScalarAsync(cancellationToken);
+ return result?.ToString() ?? "unknown";
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+
+ return "unknown";
+ }
+
+ ///
+ /// Configures the model relationships, constraints, and schema definitions for the database context.
+ /// Called when the database model is being created or initialized.
+ ///
+ /// The builder used to define the model structure and relationships.
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ if (Database.ProviderName!.Equals("MongoDB.EntityFrameworkCore"))
+ {
+ ConfigureMongoDbModel(modelBuilder);
+ }
+ else
+ {
+ ConfigureSqlModel(modelBuilder);
+ }
+
+ base.OnModelCreating(modelBuilder);
+ }
+
+ private static void ConfigureMongoDbModel(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.Property(e => e.Timestamp)
+ .HasBsonRepresentation(BsonType.DateTime);
+ entity.Property(e => e.ExecTimestamp)
+ .HasBsonRepresentation(BsonType.DateTime);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.Property(e => e.Timestamp)
+ .HasBsonRepresentation(BsonType.DateTime);
+ });
+ }
+
+ private void ConfigureSqlModel(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.OwnsOne(e => e.Trace, trace =>
+ {
+ trace.Property(e => e.Message).HasColumnName(PropertyNames.TraceMessage);
+
+ trace.OwnsOne(e => e.Engine, engine =>
+ {
+ if (Database.IsOracle())
+ {
+ engine.Property(e => e.Configs)
+ .HasColumnName(PropertyNames.EngineConfigs)
+ .HasColumnType("json");
+ }
+ else if (Database.IsNpgsql())
+ {
+ engine.Property(e => e.Configs)
+ .HasColumnName(PropertyNames.EngineConfigs)
+ .HasColumnType("jsonb");
+ }
+
+ engine.Property(e => e.Type).HasColumnName(PropertyNames.EngineType);
+ engine.Property(e => e.Version).HasColumnName(PropertyNames.EngineVersion);
+ engine.Property(e => e.SysPaths).HasColumnName(PropertyNames.EngineSysPaths);
+ });
+ });
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Infrastructure/Persistence/MongoNativeDbContext.cs b/src/Telemetry.Api/Infrastructure/Persistence/MongoNativeDbContext.cs
new file mode 100644
index 0000000..c8d03de
--- /dev/null
+++ b/src/Telemetry.Api/Infrastructure/Persistence/MongoNativeDbContext.cs
@@ -0,0 +1,75 @@
+using MongoDB.Bson;
+using MongoDB.Driver;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Reflection;
+using Telemetry.Api.Application.Interfaces;
+using Telemetry.Api.Domain.Models;
+
+namespace Telemetry.Api.Infrastructure.Persistence
+{
+ internal sealed class MongoNativeDbContext : IApplicationDbContext
+ {
+ private readonly IMongoClient _mongoClient;
+ private readonly IMongoDatabase _mongoDatabase;
+
+ private readonly IMongoCollection _eventsCollection;
+ private readonly IMongoCollection _scriptCollection;
+
+ public MongoNativeDbContext(IMongoClient mongoClient, string mongodbName)
+ {
+ _mongoClient = mongoClient;
+ _mongoDatabase = _mongoClient.GetDatabase(mongodbName);
+
+ _eventsCollection = _mongoDatabase.GetCollection(GetCollectionName());
+ _scriptCollection = _mongoDatabase.GetCollection(GetCollectionName());
+ }
+
+
+ private static string GetCollectionName()
+ {
+ var table = typeof(T).GetCustomAttribute();
+ return table?.Name ?? typeof(T).Name;
+ }
+
+ public async Task AddEventRecord(EventRecord eventRecord, CancellationToken cancellationToken)
+ {
+ await _eventsCollection.InsertOneAsync(eventRecord, new InsertOneOptions(), cancellationToken);
+ }
+
+ public async Task AddScriptRecord(ScriptRecord scriptRecord, CancellationToken cancellationToken)
+ {
+ await _scriptCollection.InsertOneAsync(scriptRecord, new InsertOneOptions(), cancellationToken);
+ }
+
+ public Task SaveChangesAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult(0);
+ }
+
+ public async Task CanConnectAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ await _mongoClient.GetDatabase("admin")
+ .RunCommandAsync(new BsonDocument("ping", 1), cancellationToken: cancellationToken);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public string GetDbProvider()
+ {
+ return "MongoDB Native";
+ }
+
+ public async Task GetDbVersionAsync(CancellationToken cancellationToken)
+ {
+ BsonDocumentCommand command = new(new BsonDocument {{"buildInfo", 1}});
+ BsonValue? version = await _mongoDatabase.RunCommandAsync(command, cancellationToken: cancellationToken);
+ return version["version"].AsString;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/JsonConverters/DynamicDataBsonSerializer.cs b/src/Telemetry.Api/JsonConverters/DynamicDataBsonSerializer.cs
new file mode 100644
index 0000000..0bd3c83
--- /dev/null
+++ b/src/Telemetry.Api/JsonConverters/DynamicDataBsonSerializer.cs
@@ -0,0 +1,25 @@
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace Telemetry.Api.JsonConverters;
+
+internal class DynamicDataBsonSerializer : SerializerBase
+{
+ public override string? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ return BsonDocumentSerializer.Instance.Deserialize(context).ToString();
+ }
+
+ public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ context.Writer.WriteNull();
+ }
+ else
+ {
+ BsonDocumentSerializer.Instance.Serialize(context, BsonDocument.Parse(value));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/JsonConverters/DynamicDataJsonConverter.cs b/src/Telemetry.Api/JsonConverters/DynamicDataJsonConverter.cs
new file mode 100644
index 0000000..8522540
--- /dev/null
+++ b/src/Telemetry.Api/JsonConverters/DynamicDataJsonConverter.cs
@@ -0,0 +1,23 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Telemetry.Api.JsonConverters;
+
+internal class DynamicDataJsonConverter : JsonConverter
+{
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ using JsonDocument parsed = JsonDocument.ParseValue(ref reader);
+ return parsed.RootElement.GetRawText();
+ }
+
+ public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value);
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Program.cs b/src/Telemetry.Api/Program.cs
new file mode 100644
index 0000000..617d0e0
--- /dev/null
+++ b/src/Telemetry.Api/Program.cs
@@ -0,0 +1,208 @@
+using Microsoft.AspNetCore.HttpLogging;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.OpenApi;
+using Prometheus;
+using Telemetry.Api.Application.Interfaces;
+using Telemetry.Api.Application.Services;
+using Telemetry.Api.Infrastructure.Persistence;
+using Serilog;
+using MongoDB.Driver;
+using System.Reflection;
+using System.Runtime.Loader;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(builder.Configuration)
+ .Enrich.FromLogContext()
+ .WriteTo.Console()
+ .CreateLogger();
+
+builder.Host.UseSerilog(Log.Logger);
+
+builder.Services.AddHttpLogging(logging =>
+{
+ logging.LoggingFields = HttpLoggingFields.RequestMethod |
+ HttpLoggingFields.RequestPath |
+ HttpLoggingFields.ResponseStatusCode |
+ HttpLoggingFields.Duration;
+
+ logging.RequestBodyLogLimit = 4096;
+ logging.ResponseBodyLogLimit = 4096;
+});
+
+builder.Services.AddDbContext(options =>
+{
+ string? connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
+ string? dbProvider = builder.Configuration.GetValue("DbProvider")?.ToLower();
+
+ if (string.IsNullOrEmpty(connectionString))
+ {
+ throw new InvalidOperationException("Missing connection string");
+ }
+
+ switch (dbProvider)
+ {
+ case "oracle":
+ Log.Information("Using Oracle database");
+ options.UseOracle(connectionString,
+ x => x.MigrationsAssembly("Telemetry.Migrations.Oracle"));
+
+ LoadAssembly(dbProvider, "Telemetry.Migrations.Oracle.dll");
+ break;
+ case "postgres":
+ Log.Information("Using PostgresSQL database");
+ options.UseNpgsql(connectionString,
+ x => x.MigrationsAssembly("Telemetry.Migrations.Postgres"));
+
+ LoadAssembly(dbProvider, "Telemetry.Migrations.Postgres.dll");
+ break;
+ case "sqlite":
+ Log.Information("Using SQLite database");
+ options.UseSqlite(connectionString,
+ x => x.MigrationsAssembly("Telemetry.Migrations.Sqlite"));
+
+ LoadAssembly(dbProvider, "Telemetry.Migrations.Sqlite.dll");
+ break;
+ case "mssql":
+ Log.Information("Using MS SQL database");
+ options.UseSqlServer(connectionString,
+ x => x.MigrationsAssembly("Telemetry.Migrations.SqlServer"));
+
+ LoadAssembly(dbProvider, "Telemetry.Migrations.SqlServer.dll");
+ break;
+ case "mongodb":
+ Log.Information("Using MongoDB database");
+ string mongoDbName = builder.Configuration.GetValue("MongoDbDatabaseName") ?? "pyrevit-telemetry";
+ options.UseMongoDB(connectionString, mongoDbName);
+
+ break;
+ case "mongodb_native":
+ break;
+ default:
+ throw new NotSupportedException($"Provider {dbProvider} is not supported.");
+ }
+});
+
+string? dbProviderValue = builder.Configuration.GetValue("DbProvider")?.ToLower();
+if (dbProviderValue?.Equals("mongodb_native") == true)
+{
+ string? connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
+ if (string.IsNullOrEmpty(connectionString))
+ {
+ throw new InvalidOperationException("Missing connection string");
+ }
+
+ string mongoDbName = builder.Configuration.GetValue("MongoDbDatabaseName") ?? "pyrevit-telemetry";
+ builder.Services.AddSingleton(new MongoClient(connectionString));
+ builder.Services.AddScoped(provider =>
+ new MongoNativeDbContext(provider.GetRequiredService(), mongoDbName));
+}
+else
+{
+ builder.Services.AddScoped(provider => provider.GetRequiredService());
+}
+
+builder.Services.AddSingleton();
+
+builder.Services.AddControllers();
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(options =>
+{
+ string basePath = AppContext.BaseDirectory;
+ options.IncludeXmlComments(Path.Combine(basePath, "Telemetry.Api.xml"));
+
+ options.SwaggerDoc("v2",
+ new OpenApiInfo
+ {
+ Version = "v2",
+ Title = "pyRevit Telemetry API",
+ Summary = "API for collecting platform usage telemetry and Revit application events from pyRevit.",
+ Description =
+ "This API provides endpoints for collecting, storing, and accessing telemetry data related to pyRevit platform usage, including user activity, command execution, performance metrics, and events raised inside the Autodesk Revit application. It is intended for monitoring usage patterns, analyzing user interactions, diagnosing issues, and auditing application behavior.",
+ Contact = new OpenApiContact {Name = "pyrevitlabs", Url = new Uri("https://www.pyrevitlabs.io/")},
+ License = new OpenApiLicense
+ {
+ Name = "GNU GPL v3",
+ Url = new Uri(
+ "https://raw.githubusercontent.com/pyrevitlabs/telemetry-server/refs/heads/main/LICENSE.md")
+ }
+ });
+});
+
+builder.Services.AddApiVersioning(options =>
+{
+ options.AssumeDefaultVersionWhenUnspecified = true;
+ options.DefaultApiVersion = new ApiVersion(2, 0);
+ options.ReportApiVersions = true;
+});
+
+builder.Services.AddVersionedApiExplorer(setup =>
+{
+ setup.GroupNameFormat = "'v'VVV";
+ setup.SubstituteApiVersionInUrl = true;
+});
+
+WebApplication app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+ app.UseSwagger();
+ app.UseSwaggerUI(c =>
+ {
+ c.SwaggerEndpoint("/swagger/v2/swagger.json", "pyRevit Telemetry API v2");
+ });
+}
+
+app.UseRouting();
+app.UseHttpMetrics();
+
+app.MapControllers();
+app.MapMetrics();
+
+try
+{
+ Log.Information("Starting migration");
+
+ using IServiceScope scope = app.Services.CreateScope();
+ ApplicationDbContext db = scope.ServiceProvider.GetRequiredService();
+
+ string? currentDbProvider = builder.Configuration.GetValue("DbProvider")?.ToLower();
+ if (currentDbProvider is not "mongodb" && currentDbProvider is not "mongodb_native")
+ {
+ await db.Database.MigrateAsync();
+ }
+
+ Log.Information("Migration completed");
+}
+catch (Exception ex)
+{
+ Log.Fatal(ex, "Migration terminated unexpectedly");
+ throw;
+}
+
+try
+{
+ app.Logger.LogInformation("Starting web host");
+ await app.RunAsync();
+}
+catch (Exception ex)
+{
+ app.Logger.LogCritical(ex, "Host terminated unexpectedly");
+}
+
+return;
+
+void LoadAssembly(string provider, string assemlyName)
+{
+ string assemblyPath = Path.Combine(Environment.CurrentDirectory, assemlyName);
+ if (File.Exists(assemblyPath))
+ {
+ Log.Information(
+ "Load {Provider} migration assembly {AssemblyPath}", provider, assemblyPath);
+ AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/Properties/launchSettings.json b/src/Telemetry.Api/Properties/launchSettings.json
new file mode 100644
index 0000000..a623894
--- /dev/null
+++ b/src/Telemetry.Api/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5030",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7191;http://localhost:5030",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Telemetry.Api/Telemetry.Api.csproj b/src/Telemetry.Api/Telemetry.Api.csproj
new file mode 100644
index 0000000..402bded
--- /dev/null
+++ b/src/Telemetry.Api/Telemetry.Api.csproj
@@ -0,0 +1,50 @@
+
+
+
+ net10.0
+ enable
+ enable
+ Linux
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Telemetry.Api/Telemetry.Api.http b/src/Telemetry.Api/Telemetry.Api.http
new file mode 100644
index 0000000..08cb095
--- /dev/null
+++ b/src/Telemetry.Api/Telemetry.Api.http
@@ -0,0 +1,93 @@
+@Telemetry_Api_HostAddress = http://localhost:8080
+
+### Get Status
+GET {{Telemetry_Api_HostAddress}}/api/v2/status
+Accept: application/json
+
+### Post Script Record
+POST {{Telemetry_Api_HostAddress}}/api/v2/scripts
+Content-Type: application/json
+
+{
+ "sessionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "meta": {
+ "schema": "2.0.0"
+ },
+ "timestamp": "2024-03-27T10:00:00Z",
+ "username": "testuser",
+ "host_user": "ad\\testuser",
+ "revitBuild": "2024.1",
+ "revit": "Revit 2024",
+ "pyrevit": "4.8.12",
+ "clone": "master",
+ "debug": false,
+ "config": true,
+ "from_gui": true,
+ "exec_id": "exec-123",
+ "exec_timestamp": "2024-03-27T10:00:05Z",
+ "commandbundle": "bundle-xyz",
+ "commandextension": "ext-abc",
+ "commandname": "TestCommand",
+ "commanduniquename": "TestUniqueName",
+ "scriptpath": "C:/scripts/test.py",
+ "trace": {
+ "message": "Script executed successfully",
+ "engine": {
+ "type": "IronPython",
+ "version": "2.7.12",
+ "sysPath": [
+ "path1",
+ "path2"
+ ],
+ "configs": {
+ "config": 1,
+ "version": 12,
+ "some_object": {
+ "hey": "hey"
+ },
+ "some_array": [
+ 1,
+ 2,
+ 3,
+ 4
+ ]
+ }
+ }
+ },
+ "commandresults": {}
+}
+
+### Post Event Record
+POST {{Telemetry_Api_HostAddress}}/api/v2/events
+Content-Type: application/json
+
+{
+ "handler_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "meta": {
+ "schema": "2.0.0"
+ },
+ "type": "AppStartup",
+ "status": "success",
+ "timestamp": "2024-03-27T10:00:00Z",
+ "username": "testuser",
+ "host_user": "ad\\testuser",
+ "revitbuild": "2024.1",
+ "revit": "Revit 2024",
+ "cancelled": false,
+ "docid": 42,
+ "doctype": "doc type",
+ "doctemplate": "doc template",
+ "docname": "doc name",
+ "docpath": "C:\\doc.rvt",
+ "projectname": "doc.rvt",
+ "projectnum": "projectnum",
+ "args": {
+ "operation": "transaction",
+ "added": 1,
+ "deleted": 2,
+ "modified": 4
+ }
+}
+
+### Get Prometheus Metrics
+GET {{Telemetry_Api_HostAddress}}/metrics
diff --git a/src/Telemetry.Api/Web/Controllers/TelemetryController.cs b/src/Telemetry.Api/Web/Controllers/TelemetryController.cs
new file mode 100644
index 0000000..4882498
--- /dev/null
+++ b/src/Telemetry.Api/Web/Controllers/TelemetryController.cs
@@ -0,0 +1,166 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Telemetry.Api.Application.Mappings;
+using Telemetry.Api.Application.DTOs;
+using Telemetry.Api.Application.Interfaces;
+using Telemetry.Api.Domain.Models;
+
+namespace Telemetry.Api.Web.Controllers
+{
+ ///
+ /// Controller
+ ///
+ [ApiController]
+ [ApiVersion("2")]
+ [Route("api/v{version:apiVersion}")]
+ public class TelemetryController : ControllerBase
+ {
+ private readonly IApplicationDbContext _context;
+ private readonly ILogger _logger;
+ private readonly IServiceInfo _serviceInfo;
+
+ ///
+ /// Creates controller.
+ ///
+ /// Context.
+ /// Logger.
+ /// Service info.
+ public TelemetryController(IApplicationDbContext context, ILogger logger,
+ IServiceInfo serviceInfo)
+ {
+ _context = context;
+ _logger = logger;
+ _serviceInfo = serviceInfo;
+ }
+
+ ///
+ /// Adds script record to DB.
+ ///
+ /// Adding script records.
+ /// Returns adding script records task.
+ [HttpPost("scripts")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ [ProducesResponseType(500)]
+ [ProducesResponseType(503)]
+ public async Task PostScript([FromBody] ScriptRecordDto dto)
+ {
+ try
+ {
+ CancellationToken ct = HttpContext.RequestAborted;
+
+ if (dto.Meta.SchemaVersion != new Version(2, 0, 0))
+ {
+ return BadRequest(new ProblemDetails()
+ {
+ Title = "Unsupported Schema Version",
+ Detail = $"Expected version 2.0.0, but received {dto.Meta.SchemaVersion}",
+ Status = StatusCodes.Status400BadRequest
+ });
+ }
+
+ ScriptRecord record = dto.ToModel();
+ await _context.AddScriptRecord(record, ct);
+
+ await _context.SaveChangesAsync(ct);
+ _logger.LogInformation("Script record saved: {ExecId} by {Username}", record.ExecId, record.Username);
+ return Ok();
+ }
+ catch (DbUpdateException ex)
+ {
+ _logger.LogError(ex, "Database error while saving script record");
+ return StatusCode(503);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unexpected error while saving script record");
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Adds event record to DB.
+ ///
+ /// Adding event records.
+ /// Returns adding event records task.
+ [HttpPost("events")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ [ProducesResponseType(500)]
+ [ProducesResponseType(503)]
+ public async Task PostEvent([FromBody] EventRecordDto dto)
+ {
+ try
+ {
+ CancellationToken ct = HttpContext.RequestAborted;
+
+ if (dto.Meta.SchemaVersion != new Version(2, 0, 0))
+ {
+ return BadRequest(new ProblemDetails()
+ {
+ Title = "Unsupported Schema Version",
+ Detail = $"Expected version 2.0.0, but received {dto.Meta.SchemaVersion}",
+ Status = StatusCodes.Status400BadRequest
+ });
+ }
+
+ EventRecord record = dto.ToModel();
+ await _context.AddEventRecord(record, ct);
+
+ await _context.SaveChangesAsync(ct);
+ _logger.LogInformation("Event record saved: {Type} by {Username}", record.EventType, record.Username);
+ return Ok();
+ }
+ catch (DbUpdateException ex)
+ {
+ _logger.LogError(ex, "Database error while saving an event record");
+ return StatusCode(503);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unexpected error while saving event record");
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Retrieves the current status.
+ ///
+ /// The current status as a status object.
+ [HttpGet("status")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ [ProducesResponseType(503)]
+ public async Task GetStatus()
+ {
+ try
+ {
+ CancellationToken ct = HttpContext.RequestAborted;
+
+ bool canConnect = await _context.CanConnectAsync(ct);
+ string provider = _context.GetDbProvider();
+ string version = await _context.GetDbVersionAsync(ct);
+
+ Dictionary checks = new()
+ {
+ {provider, new StatusCheckDto {Status = canConnect ? "pass" : "fail", Version = version}}
+ };
+
+ StatusRecordDto status = new()
+ {
+ Status = canConnect ? "pass" : "fail",
+ ServiceId = _serviceInfo.ServiceId,
+ Version = typeof(TelemetryController).Assembly.GetName().Version?.ToString() ?? "2.0.0.0",
+ Checks = checks
+ };
+
+ return canConnect ? Ok(status) : StatusCode(503, status);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error checking status");
+ return StatusCode(500);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Telemetry.Api/appsettings.Development.json b/src/Telemetry.Api/appsettings.Development.json
new file mode 100644
index 0000000..3e1a225
--- /dev/null
+++ b/src/Telemetry.Api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Information"
+ }
+ }
+}
diff --git a/src/Telemetry.Api/appsettings.json b/src/Telemetry.Api/appsettings.json
new file mode 100644
index 0000000..4282acb
--- /dev/null
+++ b/src/Telemetry.Api/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/Telemetry.Migrations.Oracle/Migrations/20260406123021_InitOracle.Designer.cs b/src/Telemetry.Migrations.Oracle/Migrations/20260406123021_InitOracle.Designer.cs
new file mode 100644
index 0000000..2520eaf
--- /dev/null
+++ b/src/Telemetry.Migrations.Oracle/Migrations/20260406123021_InitOracle.Designer.cs
@@ -0,0 +1,319 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Oracle.EntityFrameworkCore.Metadata;
+using Telemetry.Api.Infrastructure.Persistence;
+
+#nullable disable
+
+namespace Telemetry.Migrations.Oracle.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260406123021_InitOracle")]
+ partial class InitOracle
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Telemetry.Api.Domain.Models.EventRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("RAW(16)")
+ .HasColumnName("_id");
+
+ b.Property("Cancellable")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("cancellable");
+
+ b.Property("Cancelled")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("cancelled");
+
+ b.Property("DocumentId")
+ .HasColumnType("NUMBER(10)")
+ .HasColumnName("docid");
+
+ b.Property("DocumentName")
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("docname");
+
+ b.Property("DocumentPath")
+ .HasMaxLength(1024)
+ .HasColumnType("NVARCHAR2(1024)")
+ .HasColumnName("docpath");
+
+ b.Property("DocumentTemplate")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("doctemplate");
+
+ b.Property("DocumentType")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("doctype");
+
+ b.Property("EventArgs")
+ .HasMaxLength(8000)
+ .HasColumnType("NCLOB")
+ .HasColumnName("args");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("type");
+
+ b.Property("HandlerId")
+ .HasColumnType("RAW(16)")
+ .HasColumnName("handler_id");
+
+ b.Property("HostUsername")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("host_user");
+
+ b.Property("ProjectName")
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("projectname");
+
+ b.Property("ProjectNum")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("projectnum");
+
+ b.Property("RevitBuild")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("revitbuild");
+
+ b.Property("RevitVersion")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("revit");
+
+ b.Property("Status")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("status");
+
+ b.Property("Timestamp")
+ .HasColumnType("TIMESTAMP(7) WITH TIME ZONE")
+ .HasColumnName("timestamp");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("username");
+
+ b.HasKey("Id");
+
+ b.ToTable("events");
+ });
+
+ modelBuilder.Entity("Telemetry.Api.Domain.Models.ScriptRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("RAW(16)")
+ .HasColumnName("_id");
+
+ b.Property("CloneName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("clone");
+
+ b.Property("CommandBundle")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("commandbundle");
+
+ b.Property("CommandExtension")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("commandextension");
+
+ b.Property("CommandName")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("commandname");
+
+ b.Property("CommandResults")
+ .HasMaxLength(8000)
+ .HasColumnType("NCLOB")
+ .HasColumnName("commandresults");
+
+ b.Property("CommandUniqueName")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("NVARCHAR2(500)")
+ .HasColumnName("commanduniquename");
+
+ b.Property("DocumentName")
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("docname");
+
+ b.Property("DocumentPath")
+ .HasMaxLength(1024)
+ .HasColumnType("NVARCHAR2(1024)")
+ .HasColumnName("docpath");
+
+ b.Property("ExecId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("exec_id");
+
+ b.Property("ExecTimestamp")
+ .HasColumnType("TIMESTAMP(7) WITH TIME ZONE")
+ .HasColumnName("exec_timestamp");
+
+ b.Property("HostUsername")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("host_user");
+
+ b.Property("IsConfig")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("config");
+
+ b.Property("IsDebug")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("debug");
+
+ b.Property("IsExecFromGui")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("from_gui");
+
+ b.Property("PyRevitVersion")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("pyrevit");
+
+ b.Property("ResultCode")
+ .HasColumnType("NUMBER(10)")
+ .HasColumnName("resultcode");
+
+ b.Property("RevitBuild")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("revitbuild");
+
+ b.Property("RevitVersion")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("revit");
+
+ b.Property("ScriptPath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("NVARCHAR2(1024)")
+ .HasColumnName("scriptpath");
+
+ b.Property("SessionId")
+ .HasColumnType("RAW(16)")
+ .HasColumnName("sessionid");
+
+ b.Property("Timestamp")
+ .HasColumnType("TIMESTAMP(7) WITH TIME ZONE")
+ .HasColumnName("timestamp");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("username");
+
+ b.HasKey("Id");
+
+ b.ToTable("scripts");
+ });
+
+ modelBuilder.Entity("Telemetry.Api.Domain.Models.ScriptRecord", b =>
+ {
+ b.OwnsOne("Telemetry.Api.Domain.Models.TraceInfo", "Trace", b1 =>
+ {
+ b1.Property("ScriptRecordId")
+ .HasColumnType("RAW(16)");
+
+ b1.Property("Message")
+ .IsRequired()
+ .HasMaxLength(8000)
+ .HasColumnType("NCLOB")
+ .HasColumnName("message");
+
+ b1.HasKey("ScriptRecordId");
+
+ b1.ToTable("scripts");
+
+ b1.WithOwner()
+ .HasForeignKey("ScriptRecordId");
+
+ b1.OwnsOne("Telemetry.Api.Domain.Models.EngineInfo", "Engine", b2 =>
+ {
+ b2.Property("TraceInfoScriptRecordId")
+ .HasColumnType("RAW(16)");
+
+ b2.Property("Configs")
+ .HasMaxLength(8000)
+ .HasColumnType("json")
+ .HasColumnName("configs");
+
+ b2.PrimitiveCollection("SysPaths")
+ .HasColumnType("NVARCHAR2(2000)")
+ .HasColumnName("syspath");
+
+ b2.Property("Type")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("type");
+
+ b2.Property("Version")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("version");
+
+ b2.HasKey("TraceInfoScriptRecordId");
+
+ b2.ToTable("scripts");
+
+ b2.WithOwner()
+ .HasForeignKey("TraceInfoScriptRecordId");
+ });
+
+ b1.Navigation("Engine")
+ .IsRequired();
+ });
+
+ b.Navigation("Trace")
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Telemetry.Migrations.Oracle/Migrations/20260406123021_InitOracle.cs b/src/Telemetry.Migrations.Oracle/Migrations/20260406123021_InitOracle.cs
new file mode 100644
index 0000000..67c341d
--- /dev/null
+++ b/src/Telemetry.Migrations.Oracle/Migrations/20260406123021_InitOracle.cs
@@ -0,0 +1,92 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Telemetry.Migrations.Oracle.Migrations
+{
+ ///
+ public partial class InitOracle : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "events",
+ columns: table => new
+ {
+ _id = table.Column(type: "RAW(16)", nullable: false),
+ handler_id = table.Column(type: "RAW(16)", nullable: false),
+ type = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ status = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true),
+ timestamp = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false),
+ username = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ host_user = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true),
+ revitbuild = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ revit = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ cancelled = table.Column(type: "BOOLEAN", nullable: true),
+ cancellable = table.Column(type: "BOOLEAN", nullable: true),
+ docid = table.Column(type: "NUMBER(10)", nullable: false),
+ doctype = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true),
+ doctemplate = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true),
+ docname = table.Column(type: "NVARCHAR2(250)", maxLength: 250, nullable: true),
+ docpath = table.Column(type: "NVARCHAR2(1024)", maxLength: 1024, nullable: true),
+ projectname = table.Column(type: "NVARCHAR2(250)", maxLength: 250, nullable: true),
+ projectnum = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true),
+ args = table.Column(type: "NCLOB", maxLength: 8000, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_events", x => x._id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "scripts",
+ columns: table => new
+ {
+ _id = table.Column(type: "RAW(16)", nullable: false),
+ sessionid = table.Column(type: "RAW(16)", nullable: false),
+ timestamp = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false),
+ username = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ host_user = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true),
+ revitbuild = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ revit = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ pyrevit = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ clone = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ debug = table.Column(type: "BOOLEAN", nullable: false),
+ config = table.Column(type: "BOOLEAN", nullable: false),
+ from_gui = table.Column(type: "BOOLEAN", nullable: false),
+ exec_id = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ exec_timestamp = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false),
+ commandbundle = table.Column(type: "NVARCHAR2(250)", maxLength: 250, nullable: false),
+ commandextension = table.Column(type: "NVARCHAR2(250)", maxLength: 250, nullable: false),
+ commandname = table.Column(type: "NVARCHAR2(250)", maxLength: 250, nullable: false),
+ commanduniquename = table.Column(type: "NVARCHAR2(500)", maxLength: 500, nullable: false),
+ docname = table.Column(type: "NVARCHAR2(250)", maxLength: 250, nullable: true),
+ docpath = table.Column(type: "NVARCHAR2(1024)", maxLength: 1024, nullable: true),
+ resultcode = table.Column(type: "NUMBER(10)", nullable: false),
+ scriptpath = table.Column(type: "NVARCHAR2(1024)", maxLength: 1024, nullable: false),
+ message = table.Column(type: "NCLOB", maxLength: 8000, nullable: false),
+ type = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ version = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
+ syspath = table.Column(type: "NVARCHAR2(2000)", nullable: true),
+ configs = table.Column(type: "json", maxLength: 8000, nullable: true),
+ commandresults = table.Column(type: "NCLOB", maxLength: 8000, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_scripts", x => x._id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "events");
+
+ migrationBuilder.DropTable(
+ name: "scripts");
+ }
+ }
+}
diff --git a/src/Telemetry.Migrations.Oracle/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Telemetry.Migrations.Oracle/Migrations/ApplicationDbContextModelSnapshot.cs
new file mode 100644
index 0000000..8077c90
--- /dev/null
+++ b/src/Telemetry.Migrations.Oracle/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -0,0 +1,316 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Oracle.EntityFrameworkCore.Metadata;
+using Telemetry.Api.Infrastructure.Persistence;
+
+#nullable disable
+
+namespace Telemetry.Migrations.Oracle.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ partial class ApplicationDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Telemetry.Api.Domain.Models.EventRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("RAW(16)")
+ .HasColumnName("_id");
+
+ b.Property("Cancellable")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("cancellable");
+
+ b.Property("Cancelled")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("cancelled");
+
+ b.Property("DocumentId")
+ .HasColumnType("NUMBER(10)")
+ .HasColumnName("docid");
+
+ b.Property("DocumentName")
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("docname");
+
+ b.Property("DocumentPath")
+ .HasMaxLength(1024)
+ .HasColumnType("NVARCHAR2(1024)")
+ .HasColumnName("docpath");
+
+ b.Property("DocumentTemplate")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("doctemplate");
+
+ b.Property("DocumentType")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("doctype");
+
+ b.Property("EventArgs")
+ .HasMaxLength(8000)
+ .HasColumnType("NCLOB")
+ .HasColumnName("args");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("type");
+
+ b.Property("HandlerId")
+ .HasColumnType("RAW(16)")
+ .HasColumnName("handler_id");
+
+ b.Property("HostUsername")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("host_user");
+
+ b.Property("ProjectName")
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("projectname");
+
+ b.Property("ProjectNum")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("projectnum");
+
+ b.Property("RevitBuild")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("revitbuild");
+
+ b.Property("RevitVersion")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("revit");
+
+ b.Property("Status")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("status");
+
+ b.Property("Timestamp")
+ .HasColumnType("TIMESTAMP(7) WITH TIME ZONE")
+ .HasColumnName("timestamp");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("username");
+
+ b.HasKey("Id");
+
+ b.ToTable("events");
+ });
+
+ modelBuilder.Entity("Telemetry.Api.Domain.Models.ScriptRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("RAW(16)")
+ .HasColumnName("_id");
+
+ b.Property("CloneName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("clone");
+
+ b.Property("CommandBundle")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("commandbundle");
+
+ b.Property("CommandExtension")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("commandextension");
+
+ b.Property("CommandName")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("commandname");
+
+ b.Property("CommandResults")
+ .HasMaxLength(8000)
+ .HasColumnType("NCLOB")
+ .HasColumnName("commandresults");
+
+ b.Property("CommandUniqueName")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("NVARCHAR2(500)")
+ .HasColumnName("commanduniquename");
+
+ b.Property("DocumentName")
+ .HasMaxLength(250)
+ .HasColumnType("NVARCHAR2(250)")
+ .HasColumnName("docname");
+
+ b.Property("DocumentPath")
+ .HasMaxLength(1024)
+ .HasColumnType("NVARCHAR2(1024)")
+ .HasColumnName("docpath");
+
+ b.Property("ExecId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("exec_id");
+
+ b.Property("ExecTimestamp")
+ .HasColumnType("TIMESTAMP(7) WITH TIME ZONE")
+ .HasColumnName("exec_timestamp");
+
+ b.Property("HostUsername")
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("host_user");
+
+ b.Property("IsConfig")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("config");
+
+ b.Property("IsDebug")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("debug");
+
+ b.Property("IsExecFromGui")
+ .HasColumnType("BOOLEAN")
+ .HasColumnName("from_gui");
+
+ b.Property("PyRevitVersion")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("pyrevit");
+
+ b.Property("ResultCode")
+ .HasColumnType("NUMBER(10)")
+ .HasColumnName("resultcode");
+
+ b.Property("RevitBuild")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("revitbuild");
+
+ b.Property("RevitVersion")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("revit");
+
+ b.Property("ScriptPath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("NVARCHAR2(1024)")
+ .HasColumnName("scriptpath");
+
+ b.Property("SessionId")
+ .HasColumnType("RAW(16)")
+ .HasColumnName("sessionid");
+
+ b.Property("Timestamp")
+ .HasColumnType("TIMESTAMP(7) WITH TIME ZONE")
+ .HasColumnName("timestamp");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("username");
+
+ b.HasKey("Id");
+
+ b.ToTable("scripts");
+ });
+
+ modelBuilder.Entity("Telemetry.Api.Domain.Models.ScriptRecord", b =>
+ {
+ b.OwnsOne("Telemetry.Api.Domain.Models.TraceInfo", "Trace", b1 =>
+ {
+ b1.Property("ScriptRecordId")
+ .HasColumnType("RAW(16)");
+
+ b1.Property("Message")
+ .IsRequired()
+ .HasMaxLength(8000)
+ .HasColumnType("NCLOB")
+ .HasColumnName("message");
+
+ b1.HasKey("ScriptRecordId");
+
+ b1.ToTable("scripts");
+
+ b1.WithOwner()
+ .HasForeignKey("ScriptRecordId");
+
+ b1.OwnsOne("Telemetry.Api.Domain.Models.EngineInfo", "Engine", b2 =>
+ {
+ b2.Property("TraceInfoScriptRecordId")
+ .HasColumnType("RAW(16)");
+
+ b2.Property("Configs")
+ .HasMaxLength(8000)
+ .HasColumnType("json")
+ .HasColumnName("configs");
+
+ b2.PrimitiveCollection("SysPaths")
+ .HasColumnType("NVARCHAR2(2000)")
+ .HasColumnName("syspath");
+
+ b2.Property("Type")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("type");
+
+ b2.Property("Version")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("NVARCHAR2(100)")
+ .HasColumnName("version");
+
+ b2.HasKey("TraceInfoScriptRecordId");
+
+ b2.ToTable("scripts");
+
+ b2.WithOwner()
+ .HasForeignKey("TraceInfoScriptRecordId");
+ });
+
+ b1.Navigation("Engine")
+ .IsRequired();
+ });
+
+ b.Navigation("Trace")
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj b/src/Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj
new file mode 100644
index 0000000..a4640d7
--- /dev/null
+++ b/src/Telemetry.Migrations.Oracle/Telemetry.Migrations.Oracle.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Library
+ net10.0
+ enable
+ enable
+ ..\..\src\Telemetry.Api\bin\
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/src/Telemetry.Migrations.Postgres/Migrations/20260406123028_InitPostgres.Designer.cs b/src/Telemetry.Migrations.Postgres/Migrations/20260406123028_InitPostgres.Designer.cs
new file mode 100644
index 0000000..6d6affd
--- /dev/null
+++ b/src/Telemetry.Migrations.Postgres/Migrations/20260406123028_InitPostgres.Designer.cs
@@ -0,0 +1,319 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using Telemetry.Api.Infrastructure.Persistence;
+
+#nullable disable
+
+namespace Telemetry.Migrations.Postgres.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260406123028_InitPostgres")]
+ partial class InitPostgres
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Telemetry.Api.Domain.Models.EventRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("_id");
+
+ b.Property("Cancellable")
+ .HasColumnType("boolean")
+ .HasColumnName("cancellable");
+
+ b.Property("Cancelled")
+ .HasColumnType("boolean")
+ .HasColumnName("cancelled");
+
+ b.Property("DocumentId")
+ .HasColumnType("integer")
+ .HasColumnName("docid");
+
+ b.Property("DocumentName")
+ .HasMaxLength(250)
+ .HasColumnType("character varying(250)")
+ .HasColumnName("docname");
+
+ b.Property("DocumentPath")
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("docpath");
+
+ b.Property("DocumentTemplate")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("doctemplate");
+
+ b.Property("DocumentType")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("doctype");
+
+ b.Property("EventArgs")
+ .HasMaxLength(8000)
+ .HasColumnType("character varying(8000)")
+ .HasColumnName("args");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("type");
+
+ b.Property("HandlerId")
+ .HasColumnType("uuid")
+ .HasColumnName("handler_id");
+
+ b.Property("HostUsername")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("host_user");
+
+ b.Property("ProjectName")
+ .HasMaxLength(250)
+ .HasColumnType("character varying(250)")
+ .HasColumnName("projectname");
+
+ b.Property("ProjectNum")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("projectnum");
+
+ b.Property("RevitBuild")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("revitbuild");
+
+ b.Property("RevitVersion")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("revit");
+
+ b.Property("Status")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("status");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("timestamp");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("username");
+
+ b.HasKey("Id");
+
+ b.ToTable("events");
+ });
+
+ modelBuilder.Entity("Telemetry.Api.Domain.Models.ScriptRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("_id");
+
+ b.Property("CloneName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("clone");
+
+ b.Property("CommandBundle")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("character varying(250)")
+ .HasColumnName("commandbundle");
+
+ b.Property("CommandExtension")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("character varying(250)")
+ .HasColumnName("commandextension");
+
+ b.Property("CommandName")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("character varying(250)")
+ .HasColumnName("commandname");
+
+ b.Property("CommandResults")
+ .HasMaxLength(8000)
+ .HasColumnType("character varying(8000)")
+ .HasColumnName("commandresults");
+
+ b.Property("CommandUniqueName")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)")
+ .HasColumnName("commanduniquename");
+
+ b.Property("DocumentName")
+ .HasMaxLength(250)
+ .HasColumnType("character varying(250)")
+ .HasColumnName("docname");
+
+ b.Property("DocumentPath")
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("docpath");
+
+ b.Property("ExecId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("exec_id");
+
+ b.Property("ExecTimestamp")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("exec_timestamp");
+
+ b.Property("HostUsername")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("host_user");
+
+ b.Property("IsConfig")
+ .HasColumnType("boolean")
+ .HasColumnName("config");
+
+ b.Property("IsDebug")
+ .HasColumnType("boolean")
+ .HasColumnName("debug");
+
+ b.Property("IsExecFromGui")
+ .HasColumnType("boolean")
+ .HasColumnName("from_gui");
+
+ b.Property("PyRevitVersion")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("pyrevit");
+
+ b.Property("ResultCode")
+ .HasColumnType("integer")
+ .HasColumnName("resultcode");
+
+ b.Property("RevitBuild")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("revitbuild");
+
+ b.Property("RevitVersion")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("revit");
+
+ b.Property("ScriptPath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("scriptpath");
+
+ b.Property("SessionId")
+ .HasColumnType("uuid")
+ .HasColumnName("sessionid");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("timestamp");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("username");
+
+ b.HasKey("Id");
+
+ b.ToTable("scripts");
+ });
+
+ modelBuilder.Entity("Telemetry.Api.Domain.Models.ScriptRecord", b =>
+ {
+ b.OwnsOne("Telemetry.Api.Domain.Models.TraceInfo", "Trace", b1 =>
+ {
+ b1.Property("ScriptRecordId")
+ .HasColumnType("uuid");
+
+ b1.Property("Message")
+ .IsRequired()
+ .HasMaxLength(8000)
+ .HasColumnType("character varying(8000)")
+ .HasColumnName("message");
+
+ b1.HasKey("ScriptRecordId");
+
+ b1.ToTable("scripts");
+
+ b1.WithOwner()
+ .HasForeignKey("ScriptRecordId");
+
+ b1.OwnsOne("Telemetry.Api.Domain.Models.EngineInfo", "Engine", b2 =>
+ {
+ b2.Property("TraceInfoScriptRecordId")
+ .HasColumnType("uuid");
+
+ b2.Property("Configs")
+ .HasMaxLength(8000)
+ .HasColumnType("jsonb")
+ .HasColumnName("configs");
+
+ b2.PrimitiveCollection("SysPaths")
+ .HasColumnType("text[]")
+ .HasColumnName("syspath");
+
+ b2.Property("Type")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("type");
+
+ b2.Property("Version")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("version");
+
+ b2.HasKey("TraceInfoScriptRecordId");
+
+ b2.ToTable("scripts");
+
+ b2.WithOwner()
+ .HasForeignKey("TraceInfoScriptRecordId");
+ });
+
+ b1.Navigation("Engine")
+ .IsRequired();
+ });
+
+ b.Navigation("Trace")
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Telemetry.Migrations.Postgres/Migrations/20260406123028_InitPostgres.cs b/src/Telemetry.Migrations.Postgres/Migrations/20260406123028_InitPostgres.cs
new file mode 100644
index 0000000..899eb80
--- /dev/null
+++ b/src/Telemetry.Migrations.Postgres/Migrations/20260406123028_InitPostgres.cs
@@ -0,0 +1,92 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Telemetry.Migrations.Postgres.Migrations
+{
+ ///
+ public partial class InitPostgres : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "events",
+ columns: table => new
+ {
+ _id = table.Column(type: "uuid", nullable: false),
+ handler_id = table.Column(type: "uuid", nullable: false),
+ type = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ status = table.Column(type: "character varying(100)", maxLength: 100, nullable: true),
+ timestamp = table.Column(type: "timestamp with time zone", nullable: false),
+ username = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ host_user = table.Column(type: "character varying(100)", maxLength: 100, nullable: true),
+ revitbuild = table.Column