Pull request #1: Initial contribution
Merge in DBMANGO/dbmango from feature/initial-contrib to db-feature/initial-contrib * commit '3d78f7c825e0ac8d2d25aa5ed7763fd58704ec6e': Resolved some code review findings Support for non-admin commands to be available to non-admin users Minor UI fixes Initial contribution
This commit is contained in:
commit
2e4a927cdf
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# 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
|
||||
373
.gitignore
vendored
Normal file
373
.gitignore
vendored
Normal file
@ -0,0 +1,373 @@
|
||||
## 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
|
||||
|
||||
# ANTLR generated
|
||||
.antlr/
|
||||
|
||||
# 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
|
||||
/.vscode
|
||||
launchSettings.json
|
||||
Resources/Antlr4/
|
||||
|
||||
# for OSS version of dbMango:
|
||||
Resources/
|
||||
nuget.config
|
||||
203
APACHE-2.0.md
Normal file
203
APACHE-2.0.md
Normal file
@ -0,0 +1,203 @@
|
||||
> Included copy of Apache-2.0 license. This is NOT a license of `dbMango`!
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
16
COPYRIGHT.txt
Normal file
16
COPYRIGHT.txt
Normal file
@ -0,0 +1,16 @@
|
||||
/* dbMango
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
38
Directory.Build.props
Normal file
38
Directory.Build.props
Normal file
@ -0,0 +1,38 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>1.0.0</Version>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<NoWarn>1591,3021,NU3018,BL0007,SYSLIB0050</NoWarn>
|
||||
<SolutionDirectory>$([MSBuild]::GetDirectoryNameOfFileAbove(`$(MSBuildProjectDirectory)`, `Directory.Build.props`))/</SolutionDirectory>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
<BaseIntermediateOutputPath>$(SolutionDirectory)bin/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
|
||||
<!-- UseCommonOutputDirectory disabled for now as there is an msbuild bug that prevents nuget packages being copied over
|
||||
<UseCommonOutputDirectory>true</UseCommonOutputDirectory>
|
||||
-->
|
||||
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
|
||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- UserSecretsId must match the one from GlobalAssemblyInfo.cs -->
|
||||
<UserSecretsId>BF0D1460-2D1E-40CC-8F10-127369F684FE</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<OutputPath>$(SolutionDirectory)bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<OutputPath>$(SolutionDirectory)bin\Release\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(SolutionDirectory)\GlobalAssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
43
Directory.Packages.props
Normal file
43
Directory.Packages.props
Normal file
@ -0,0 +1,43 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Antlr4.Runtime.Standard" Version="4.13.1" />
|
||||
<PackageVersion Include="Antlr4BuildTasks" Version="12.10.0" />
|
||||
<PackageVersion Include="BlazorDateRangePicker" Version="6.2.0" />
|
||||
<PackageVersion Include="Blazored.Modal" Version="7.3.1" />
|
||||
<PackageVersion Include="Blazorise.Splitter" Version="1.7.3" />
|
||||
<PackageVersion Include="ChartJs.Blazor.Fork" Version="2.0.2" />
|
||||
<PackageVersion Include="Grpc.AspNetCore" Version="2.71.0" />
|
||||
<PackageVersion Include="log4net" Version="3.2.0" />
|
||||
<PackageVersion Include="Markdig" Version="0.43.0" />
|
||||
<PackageVersion Include="Markdown.ColorCode" Version="3.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Grpc.Swagger" Version="0.9.10" />
|
||||
<PackageVersion Include="AspNetCore.HealthChecks.OpenIdConnectServer" Version="9.0.0" />
|
||||
<PackageVersion Include="AspNetCore.HealthChecks.Oracle" Version="9.0.0" />
|
||||
<PackageVersion Include="AspNetCore.HealthChecks.Uris" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Certificate" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Grpc.JsonTranscoding" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="MongoDB.Driver" Version="3.5.0" />
|
||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||
<PackageVersion Include="NUnit" Version="4.4.0" />
|
||||
<PackageVersion Include="NUnit.Analyzers" Version="4.10.0" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="5.2.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="Novell.Directory.Ldap.NETStandard" Version="4.0.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.4.0-rc.4" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
|
||||
<PackageVersion Include="Oracle.ManagedDataAccess.Core" Version="23.26.0" />
|
||||
<PackageVersion Include="System.DirectoryServices.AccountManagement" Version="9.0.1" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="9.0.10" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
39
GlobalAssemblyInfo.cs
Normal file
39
GlobalAssemblyInfo.cs
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Reflection;
|
||||
|
||||
// This assembly info file is used by all POD assemblies, it contains all common settings
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Deutsche Bank AG")]
|
||||
[assembly: AssemblyCopyright("Copyright © Deutsche Bank AG 2025")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: AssemblyProduct("dbMango")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// These values are currently stamped via teamcity
|
||||
[assembly: AssemblyVersion("0.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("0.0.0.0")]
|
||||
[assembly: AssemblyInformationalVersion("0.0.0.0-commit")]
|
||||
385
OSS-LICENSES.md
Normal file
385
OSS-LICENSES.md
Normal file
@ -0,0 +1,385 @@
|
||||
# Open Source Software Summary
|
||||
|
||||
| Software Name | License Type |
|
||||
|--------------------------------|--------------------|
|
||||
| CodeMirror | MIT |
|
||||
| Open Iconic | MIT |
|
||||
| Font Awesome | CC BY 4.0, SIL OFL 1.1, MIT |
|
||||
| ChartJS | MIT |
|
||||
| ChartJs.Blazor.Fork | MIT |
|
||||
| ANTLR 4 | BSD-3-Clause |
|
||||
| Blazor Range Picker | MIT |
|
||||
| Blazored Modal | MIT |
|
||||
| Blazorise.Splitter | Apache-2.0 |
|
||||
| GRPC | Apache-2.0 |
|
||||
| Log4net | Apache-2.0 |
|
||||
| Markdig | BSD-2-Clause |
|
||||
| MongoDB.Driver | Apache-2.0 |
|
||||
| Moq | BSD-3-Clause |
|
||||
| Nunit | MIT |
|
||||
| Newtonsoft.Json | MIT |
|
||||
| Novell.Directory.Ldap.NETStandard | MIT |
|
||||
| OpenTelemetry | Apache-2.0 |
|
||||
| Oracle.ManagedDataAccess.Core | Oracle License Agreement |
|
||||
|
||||
# Open Source Software used
|
||||
|
||||
In no particular order:
|
||||
|
||||
## CodeMirror
|
||||
|
||||
https://codemirror.net/
|
||||
https://github.com/codemirror/dev/
|
||||
|
||||
License: [MIT](https://github.com/codemirror/dev/?tab=License-1-ov-file#readme)
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2018 by Marijn Haverbeke <marijn@haverbeke.berlin>, Adrian
|
||||
Heine <mail@adrianheine.de>, and others
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
## Open Iconic
|
||||
|
||||
[Open Iconic v1.1.1](https://github.com/iconic/open-iconic)
|
||||
License: [MIT](https://github.com/iconic/open-iconic?tab=MIT-1-ov-file)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Waybury
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
## Font Awesome
|
||||
|
||||
Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
|
||||
|
||||
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
|
||||
## ChartJS
|
||||
|
||||
https://www.chartjs.org/
|
||||
https://github.com/chartjs/Chart.js
|
||||
|
||||
License: [MIT](https://github.com/chartjs/Chart.js?tab=MIT-1-ov-file#readme)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2024 Chart.js Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## ChartJs.Blazor.Fork
|
||||
|
||||
https://github.com/mariusmuntean/ChartJs.Blazor
|
||||
https://www.iheartblazor.com/welcome
|
||||
|
||||
License: [MIT](https://github.com/mariusmuntean/ChartJs.Blazor?tab=MIT-1-ov-file)
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Marius Muntean
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## ANTLR 4
|
||||
|
||||
https://github.com/antlr/antlr4
|
||||
License: [BSD-3-Clause](https://github.com/antlr/antlr4?tab=BSD-3-Clause-1-ov-file#readme)
|
||||
|
||||
Copyright (c) 2012-2022 The ANTLR Project. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither name of copyright holders nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## Blazor Range Picker
|
||||
|
||||
https://www.nuget.org/packages/BlazorDateRangePicker
|
||||
|
||||
License: [MIT](https://licenses.nuget.org/MIT)
|
||||
|
||||
## Blazored Modal
|
||||
|
||||
https://github.com/Blazored/Modal
|
||||
|
||||
License: [MIT](https://github.com/Blazored/Modal?tab=MIT-1-ov-file#readme)
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Blazored
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## Blazorise.Splitter
|
||||
|
||||
https://blazorise.com/docs/extensions/splitter
|
||||
https://github.com/Megabit/Blazorise
|
||||
|
||||
License:
|
||||
* https://blazorise.com/files/licences/SLA-2023-07.pdf
|
||||
* https://opensource.org/license/apache-2-0
|
||||
|
||||
> If you wish to use the Community Plan/License of the Software, you may
|
||||
download and access the source and/or binaries at no charge or payment
|
||||
under the APACHE License (the “APACHE”)
|
||||
|
||||
## GRPC
|
||||
|
||||
https://github.com/grpc/grpc-dotnet
|
||||
|
||||
License: [Apache-2.0](https://licenses.nuget.org/Apache-2.0)
|
||||
|
||||
Included copy: [APACHE-2.0.md](APACHE-2.0.md)
|
||||
|
||||
## Log4net
|
||||
|
||||
https://logging.apache.org/log4net/index.html
|
||||
|
||||
License: [Apache-2.0](https://licenses.nuget.org/Apache-2.0)
|
||||
|
||||
Included copy: [APACHE-2.0.md](APACHE-2.0.md)
|
||||
|
||||
## Markdig
|
||||
|
||||
https://github.com/xoofx/markdig
|
||||
|
||||
License: [BSD-2-Clause](https://github.com/xoofx/markdig?tab=BSD-2-Clause-1-ov-file#readme)
|
||||
|
||||
Copyright (c) 2018-2019, Alexandre Mutel
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification
|
||||
, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## MongoDB.Driver
|
||||
|
||||
https://www.mongodb.com/docs/drivers/csharp/current/
|
||||
|
||||
License: [Apache-2.0](https://licenses.nuget.org/Apache-2.0)
|
||||
|
||||
Included copy: [APACHE-2.0.md](APACHE-2.0.md)
|
||||
|
||||
## Moq
|
||||
|
||||
https://github.com/devlooped/moq
|
||||
|
||||
License: [BSD-3-Clause](https://github.com/devlooped/moq?tab=License-1-ov-file#readme)
|
||||
|
||||
BSD-3-Clause
|
||||
|
||||
Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD,
|
||||
and Contributors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the names of the copyright holders nor the names of its
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## Nunit
|
||||
|
||||
https://nunit.org/
|
||||
https://github.com/nunit/nunit
|
||||
|
||||
License: [MIT](https://github.com/nunit/nunit?tab=MIT-1-ov-file#readme)
|
||||
|
||||
Copyright (c) 2024 Charlie Poole, Rob Prouse
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
## Newtonsoft.Json
|
||||
|
||||
https://www.newtonsoft.com/json
|
||||
https://github.com/JamesNK/Newtonsoft.Json
|
||||
|
||||
License: [MIT](https://github.com/JamesNK/Newtonsoft.Json?tab=MIT-1-ov-file#readme)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2007 James Newton-King
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## Novell.Directory.Ldap.NETStandard
|
||||
|
||||
https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard
|
||||
|
||||
License: [MIT](https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard?tab=MIT-1-ov-file#readme)
|
||||
|
||||
The MIT License
|
||||
Copyright (c) 2003 Novell Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## OpenTelemetry
|
||||
|
||||
https://opentelemetry.io/
|
||||
https://github.com/open-telemetry/opentelemetry-dotnet
|
||||
|
||||
License: [Apache-2.0](https://github.com/open-telemetry/opentelemetry-dotnet?tab=Apache-2.0-1-ov-file#readme)
|
||||
|
||||
Included copy: [APACHE-2.0.md](APACHE-2.0.md)
|
||||
|
||||
## Oracle.ManagedDataAccess.Core
|
||||
|
||||
https://www.oracle.com/database/technologies/appdev/dotnet.html
|
||||
|
||||
License: [Oracle license agreement](https://www.nuget.org/packages/Oracle.ManagedDataAccess.Core/23.9.1/License)
|
||||
|
||||
|
||||
|
||||
|
||||
102
Rms.Risk.Mango.Interfaces/DatabasesConfig.cs
Normal file
102
Rms.Risk.Mango.Interfaces/DatabasesConfig.cs
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using Rms.Risk.Mango.Pivot.Core.MongoDb;
|
||||
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration for multiple databases.
|
||||
/// </summary>
|
||||
public class DatabasesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the configuration for a single database.
|
||||
/// </summary>
|
||||
public class DatabaseConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents LDAP group configurations for a database.
|
||||
/// </summary>
|
||||
public class LdapGroups
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP group for administrators.
|
||||
/// </summary>
|
||||
public string Admin { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP group for read-only access.
|
||||
/// </summary>
|
||||
public string ReadOnly { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP group for read-write access.
|
||||
/// </summary>
|
||||
public string ReadWrite { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep copy of the current <see cref="LdapGroups"/> instance.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="LdapGroups"/> instance with the same values.</returns>
|
||||
public LdapGroups Clone()
|
||||
=> new()
|
||||
{
|
||||
Admin = Admin,
|
||||
ReadOnly = ReadOnly,
|
||||
ReadWrite = ReadWrite
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP group configurations.
|
||||
/// </summary>
|
||||
public LdapGroups Groups { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MongoDB configuration record.
|
||||
/// </summary>
|
||||
public MongoDbConfigRecord Config { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the contact information for the database.
|
||||
/// </summary>
|
||||
public string Contacts { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep copy of the current <see cref="DatabaseConfig"/> instance.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="DatabaseConfig"/> instance with the same values.</returns>
|
||||
public DatabaseConfig Clone()
|
||||
{
|
||||
var c = new DatabaseConfig
|
||||
{
|
||||
Config = Config.Clone(),
|
||||
Groups = Groups.Clone(),
|
||||
Contacts = Contacts
|
||||
};
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dictionary of database configurations, keyed by database name.
|
||||
/// </summary>
|
||||
// ReSharper disable once CollectionNeverUpdated.Global
|
||||
public Dictionary<string, DatabaseConfig> Databases { get; set; } = new();
|
||||
}
|
||||
95
Rms.Risk.Mango.Interfaces/DbMangoDatabaseConfigContext.cs
Normal file
95
Rms.Risk.Mango.Interfaces/DbMangoDatabaseConfigContext.cs
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the parameters required for database configuration.
|
||||
/// </summary>
|
||||
public class DatabaseParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the contact information for the database.
|
||||
/// </summary>
|
||||
public string Contacts { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MongoDB connection URL.
|
||||
/// </summary>
|
||||
public string MongoDbUrl { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MongoDB database name.
|
||||
/// </summary>
|
||||
public string MongoDbDatabase { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username for user authentication.
|
||||
/// </summary>
|
||||
public string UserAuthUser { get; set; } = "admin";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password for user authentication.
|
||||
/// </summary>
|
||||
public string UserAuthPassword { get; set; } = "admin";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication database for user authentication.
|
||||
/// </summary>
|
||||
public string? UserAuthAuthDatabase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication method for user authentication.
|
||||
/// </summary>
|
||||
public string? UserAuthMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username for admin authentication.
|
||||
/// </summary>
|
||||
public string AdminAuthUser { get; set; } = "admin";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password for admin authentication.
|
||||
/// </summary>
|
||||
public string AdminAuthPassword { get; set; } = "admin";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication database for admin authentication.
|
||||
/// </summary>
|
||||
public string? AdminAuthAuthDatabase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication method for admin authentication.
|
||||
/// </summary>
|
||||
public string? AdminAuthMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use a direct connection to the database.
|
||||
/// </summary>
|
||||
public bool DirectConnection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use TLS for the connection.
|
||||
/// </summary>
|
||||
public bool UseTls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether shard access is allowed.
|
||||
/// </summary>
|
||||
public bool AllowShardAccess { get; set; }
|
||||
}
|
||||
69
Rms.Risk.Mango.Interfaces/IAuditService.cs
Normal file
69
Rms.Risk.Mango.Interfaces/IAuditService.cs
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an audit record containing details about a database operation.
|
||||
/// </summary>
|
||||
/// <param name="DatabaseName">The name of the database where the operation occurred.</param>
|
||||
/// <param name="Timestamp">The timestamp of when the operation was performed.</param>
|
||||
/// <param name="Email">The email of the user who performed the operation.</param>
|
||||
/// <param name="Ticket">The ticket identifier associated with the operation.</param>
|
||||
/// <param name="Success">Indicates whether the operation was successful.</param>
|
||||
/// <param name="Command">The MongoDB command executed during the operation.</param>
|
||||
/// <param name="Error">Optional error message if the operation failed.</param>
|
||||
public record AuditRecord(
|
||||
string DatabaseName,
|
||||
DateTime Timestamp,
|
||||
string Email,
|
||||
string Ticket,
|
||||
bool Success,
|
||||
BsonDocument Command,
|
||||
string? Error = null);
|
||||
|
||||
/// <summary>
|
||||
/// Defines methods for auditing database operations.
|
||||
/// </summary>
|
||||
public interface IAuditService
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs a pre-check on the provided MongoDB command.
|
||||
/// </summary>
|
||||
/// <param name="command">The MongoDB command to validate before execution.</param>
|
||||
void PreCheck(BsonDocument command);
|
||||
|
||||
/// <summary>
|
||||
/// Records an audit entry for a database operation.
|
||||
/// </summary>
|
||||
/// <param name="rec">The audit record containing details of the operation.</param>
|
||||
/// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task Record(AuditRecord rec, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of audit records within a specified date range.
|
||||
/// </summary>
|
||||
/// <param name="startDate">The start date of the range to retrieve audit records.</param>
|
||||
/// <param name="endDate">The end date of the range to retrieve audit records.</param>
|
||||
/// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing a list of audit records.</returns>
|
||||
Task<List<AuditRecord>> Audit(DateTime startDate, DateTime endDate, CancellationToken token = default);
|
||||
}
|
||||
26
Rms.Risk.Mango.Interfaces/IChangeNumberChecker.cs
Normal file
26
Rms.Risk.Mango.Interfaces/IChangeNumberChecker.cs
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
public record CheckerReply(bool IsValid, string? ErrorMessage = null, DateTime ValidFromUtc = default, DateTime ValidToUtc = default);
|
||||
|
||||
public interface IChangeNumberChecker
|
||||
{
|
||||
Task<CheckerReply> IsValid(string taskNumber, string email, DateTime whenTimeUtc = default);
|
||||
}
|
||||
138
Rms.Risk.Mango.Interfaces/IDatabaseConfigurationStorage.cs
Normal file
138
Rms.Risk.Mango.Interfaces/IDatabaseConfigurationStorage.cs
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the base context for database configurations.
|
||||
/// </summary>
|
||||
public class ContextBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the context.
|
||||
/// </summary>
|
||||
public string Type { get; set; } = "dbMangoDatabase";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the context.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the context.
|
||||
/// </summary>
|
||||
public long ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the parent context.
|
||||
/// </summary>
|
||||
public long ParentID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the context is a template.
|
||||
/// </summary>
|
||||
public bool IsTemplate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the context is proposed for deletion.
|
||||
/// </summary>
|
||||
public bool ProposedForDeletion { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the database configuration context for a Mango database.
|
||||
/// </summary>
|
||||
public class DbMangoDatabaseConfigContext : ContextBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the database parameters for the configuration.
|
||||
/// </summary>
|
||||
public DatabaseParams DatabaseParams { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP group parameters for the configuration.
|
||||
/// </summary>
|
||||
public DatabasesConfig.DatabaseConfig.LdapGroups LdapParams { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Interface for managing database configuration storage operations.
|
||||
/// </summary>
|
||||
public interface IDatabaseConfigurationStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads a database configuration by its identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the database configuration.</param>
|
||||
/// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the database configuration context.</returns>
|
||||
Task<DbMangoDatabaseConfigContext> Read(long id, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new database configuration.
|
||||
/// </summary>
|
||||
/// <param name="ctx">The database configuration context to create.</param>
|
||||
/// <param name="email">The email of the user performing the operation.</param>
|
||||
/// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the unique identifier of the created configuration.</returns>
|
||||
Task<long> Create(DbMangoDatabaseConfigContext ctx, string email, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing database configuration.
|
||||
/// </summary>
|
||||
/// <param name="ctx">The database configuration context to update.</param>
|
||||
/// <param name="email">The email of the user performing the operation.</param>
|
||||
/// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task Update(DbMangoDatabaseConfigContext ctx, string email, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a database configuration by its identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the database configuration to delete.</param>
|
||||
/// <param name="email">The email of the user performing the operation.</param>
|
||||
/// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task Delete(long id, string email, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all database configurations associated with a specific user.
|
||||
/// </summary>
|
||||
/// <param name="email">The email of the user whose configurations are to be listed.</param>
|
||||
/// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains a list of database configuration contexts.</returns>
|
||||
Task<List<DbMangoDatabaseConfigContext>> List(string email, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves configuration settings for a specific user.
|
||||
/// </summary>
|
||||
/// <param name="email">The email of the user whose configuration settings are to be retrieved.</param>
|
||||
/// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains a dictionary of configuration settings.</returns>
|
||||
Task<Dictionary<string, string>> GetConfiguration(string email, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Updates configuration settings for a specific user.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The dictionary of configuration settings to update.</param>
|
||||
/// <param name="email">The email of the user performing the operation.</param>
|
||||
/// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task UpdateConfiguration(Dictionary<string, string> configuration, string email, CancellationToken token);
|
||||
}
|
||||
41
Rms.Risk.Mango.Interfaces/IDbMangoPlugin.cs
Normal file
41
Rms.Risk.Mango.Interfaces/IDbMangoPlugin.cs
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for a plugin that integrates with dbMango.
|
||||
/// </summary>
|
||||
public interface IDbMangoPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the services required by the plugin.
|
||||
/// </summary>
|
||||
/// <param name="builder">The application builder used to configure services.</param>
|
||||
/// <returns>The updated application builder.</returns>
|
||||
IHostApplicationBuilder ConfigureServices(IHostApplicationBuilder builder);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a secure audit service using the provided Oracle connection settings.
|
||||
/// </summary>
|
||||
/// <param name="settings">The Oracle connection settings used to configure the audit service.</param>
|
||||
/// <returns>An instance of <see cref="IAuditService"/> if successful; otherwise, <c>null</c>.</returns>
|
||||
IAuditService? CreateSecureAuditService(OracleConnectionSettings settings);
|
||||
}
|
||||
145
Rms.Risk.Mango.Interfaces/IUserSession.cs
Normal file
145
Rms.Risk.Mango.Interfaces/IUserSession.cs
Normal file
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
using Rms.Risk.Mango.Pivot.Core;
|
||||
using Rms.Risk.Mango.Pivot.Core.MongoDb;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Rms.Service.Bootstrap.Security;
|
||||
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing a user session, providing access to user-related services, database configurations,
|
||||
/// and MongoDB services.
|
||||
/// </summary>
|
||||
public interface IUserSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the user service for the current session.
|
||||
/// </summary>
|
||||
UserService User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the task number associated with the session.
|
||||
/// </summary>
|
||||
string? TaskNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error message, if any, related to task checks.
|
||||
/// </summary>
|
||||
string? TaskCheckError { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the database being used in the session.
|
||||
/// </summary>
|
||||
string Database { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether database instance selection is allowed.
|
||||
/// </summary>
|
||||
bool IsDatabaseInstanceSelectionAllowed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the collection being used in the session.
|
||||
/// </summary>
|
||||
string Collection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the database instance being used in the session.
|
||||
/// </summary>
|
||||
string DatabaseInstance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MongoDB service for interacting with the database.
|
||||
/// </summary>
|
||||
IMongoDbService<BsonDocument> MongoDb { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MongoDB admin service for managing the database.
|
||||
/// </summary>
|
||||
IMongoDbDatabaseAdminService MongoDbAdmin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MongoDB admin service for managing the admin database.
|
||||
/// </summary>
|
||||
IMongoDbDatabaseAdminService MongoDbAdminForAdminDatabase { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pivot table data source for the session.
|
||||
/// </summary>
|
||||
IPivotTableDataSource PivotDataSource { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audit service for logging and tracking changes.
|
||||
/// </summary>
|
||||
IAuditService Audit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration record for the database.
|
||||
/// </summary>
|
||||
MongoDbConfigRecord DatabaseConfig { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the LDAP groups configuration for the session.
|
||||
/// </summary>
|
||||
DatabasesConfig.DatabaseConfig.LdapGroups LdapGroups { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the session has a valid task.
|
||||
/// </summary>
|
||||
/// <returns>A task that resolves to true if the task is valid; otherwise, false.</returns>
|
||||
Task<bool> HasValidTask();
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the user can access a specific resource based on the provided policy and database name.
|
||||
/// </summary>
|
||||
/// <param name="auth">The authorization service.</param>
|
||||
/// <param name="policyName">The name of the policy to check.</param>
|
||||
/// <param name="databaseName">The name of the database to check access for.</param>
|
||||
/// <returns>A task that resolves to true if access is allowed; otherwise, false.</returns>
|
||||
Task<bool> CanAccess(IAuthorizationService auth, string policyName, string databaseName);
|
||||
|
||||
/// <summary>
|
||||
/// Determines if instance selection is allowed for the specified database.
|
||||
/// </summary>
|
||||
/// <param name="database">The name of the database.</param>
|
||||
/// <returns>True if instance selection is allowed; otherwise, false.</returns>
|
||||
bool IsInstanceSelectionAllowed(string database);
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when the database changes.
|
||||
/// </summary>
|
||||
event Action? DatabaseChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a custom MongoDB admin service for the specified database and instance.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <param name="databaseInstance">The name of the database instance.</param>
|
||||
/// <returns>The custom MongoDB admin service.</returns>
|
||||
IMongoDbDatabaseAdminService GetCustomAdmin(string databaseName, string databaseInstance);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a shard connection for the specified host and port.
|
||||
/// </summary>
|
||||
/// <param name="host">The host of the shard.</param>
|
||||
/// <param name="port">The port of the shard.</param>
|
||||
/// <returns>The MongoDB admin service for the shard connection.</returns>
|
||||
IMongoDbDatabaseAdminService GetShardConnection(string host, int port);
|
||||
}
|
||||
54
Rms.Risk.Mango.Interfaces/MenuService.cs
Normal file
54
Rms.Risk.Mango.Interfaces/MenuService.cs
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
public record MenuItem
|
||||
(
|
||||
string Menu,
|
||||
string Title,
|
||||
string Url
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods to manage and retrieve menu items and menus.
|
||||
/// </summary>
|
||||
public interface IMenuService
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new menu item to a specified menu.
|
||||
/// </summary>
|
||||
/// <param name="menu">The name of the menu to which the item will be added.</param>
|
||||
/// <param name="title">The title of the menu item.</param>
|
||||
/// <param name="url">The URL associated with the menu item.</param>
|
||||
public void AddMenuItem(string menu, string title, string url);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all menu items for a specified menu.
|
||||
/// </summary>
|
||||
/// <param name="menu">The name of the menu whose items are to be retrieved.</param>
|
||||
/// <returns>A list of <see cref="MenuItem"/> objects for the specified menu.</returns>
|
||||
public List<MenuItem> Get(string menu);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of all available menu names.
|
||||
/// </summary>
|
||||
/// <returns>A list of menu names.</returns>
|
||||
public List<string> GetMenus();
|
||||
}
|
||||
|
||||
87
Rms.Risk.Mango.Interfaces/MongoDbCommandHelper.cs
Normal file
87
Rms.Risk.Mango.Interfaces/MongoDbCommandHelper.cs
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for MongoDB command operations.
|
||||
/// </summary>
|
||||
public static class MongoDbCommandHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of MongoDB command names that are considered read-only and do not require auditing.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> _noAudit = new(StringComparer.InvariantCultureIgnoreCase)
|
||||
{
|
||||
"find",
|
||||
"aggregate",
|
||||
"listcollections",
|
||||
"listDatabases",
|
||||
"hello",
|
||||
"listIndexes",
|
||||
"collStats",
|
||||
"listCommands",
|
||||
"ping",
|
||||
"listShards",
|
||||
"getShardMap",
|
||||
"serverStatus",
|
||||
"balancerStatus",
|
||||
"dbStats",
|
||||
"buildInfo",
|
||||
"getShardVersion",
|
||||
"getLog",
|
||||
"rolesInfo",
|
||||
"usersInfo",
|
||||
"availableQueryOptions",
|
||||
"analyzeShardKey",
|
||||
"analyze",
|
||||
"currentOp",
|
||||
"connectionStatus",
|
||||
"replSetGetStatus",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified MongoDB command is read-only.
|
||||
/// </summary>
|
||||
/// <param name="command">The MongoDB command represented as a <see cref="BsonDocument"/>.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the command is read-only; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsReadOnlyCommand(BsonDocument command)
|
||||
{
|
||||
if (command == null || command.IsBsonNull)
|
||||
return false;
|
||||
var commandType = command.ElementAt(0).Name;
|
||||
return IsReadOnlyCommand(commandType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified MongoDB command name is read-only.
|
||||
/// </summary>
|
||||
/// <param name="command">The name of the MongoDB command.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the command name is read-only; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsReadOnlyCommand(string? command)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(command) && _noAudit.Contains(command)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
55
Rms.Risk.Mango.Interfaces/OracleConnectionSettings.cs
Normal file
55
Rms.Risk.Mango.Interfaces/OracleConnectionSettings.cs
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the settings required to establish a connection to an Oracle database.
|
||||
/// </summary>
|
||||
public class OracleConnectionSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the connection string for the Oracle database.
|
||||
/// </summary>
|
||||
public string ConnectionString { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password for the Oracle database connection.
|
||||
/// </summary>
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection string for the audit database, if applicable.
|
||||
/// </summary>
|
||||
public string? AuditConnectionString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password for the audit database connection, if applicable.
|
||||
/// </summary>
|
||||
public string? AuditPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the Oracle wallet, if applicable.
|
||||
/// </summary>
|
||||
public string? Wallet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TNS_ADMIN directory path, if applicable.
|
||||
/// </summary>
|
||||
public string? TnsAdmin { get; set; }
|
||||
}
|
||||
23
Rms.Risk.Mango.Interfaces/README.md
Normal file
23
Rms.Risk.Mango.Interfaces/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
### This is a public interface for dbMango plugin.
|
||||
|
||||
## Purpose
|
||||
|
||||
> Implementation is optional!
|
||||
Upon startup dbMango will look for the plugin and if it is not found,
|
||||
it will continue without it.
|
||||
|
||||
On startup dbMango will look for assembly called `Rms.Risk.Mango.Db.Plugin.dll`. If it exists,
|
||||
it will be loaded and the plugin will be initialized. The plugin is the class with the full name of
|
||||
`Rms.Risk.Mango.Db.Plugin.Plugin`. This name must match the plugin name mask what is hardcoded in dbMango code.
|
||||
|
||||
For more information about creating plugins see [dbMango Plugins](../Rms.Risk.Mango/wwwroot/docs/plugins.md).
|
||||
|
||||
## Functionality
|
||||
|
||||
Currently it only provides interfaces for persistent storage for
|
||||
|
||||
- configuration parameters overrides stored externally (Oracle database)
|
||||
- persistent storage for onboarding information
|
||||
- secure storage for audit records
|
||||
|
||||
The only implementation is in Rms.Risk.Mango.Db.Plugin for Deutsche Bank specific implementation.
|
||||
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Rms.Risk.Mango.Pivot.Core\Rms.Risk.Mango.Pivot.Core.csproj" />
|
||||
<ProjectReference Include="..\Rms.Service.Bootstrap\Rms.Service.Bootstrap.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
36
Rms.Risk.Mango.Language/Ast/AstAggregation.cs
Normal file
36
Rms.Risk.Mango.Language/Ast/AstAggregation.cs
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstAggregation(string _collection) : AstNodeBase
|
||||
{
|
||||
public string Collection => _collection.Trim('\"');
|
||||
|
||||
public AstPipeline? Pipeline => Children.OfType<AstPipeline>().FirstOrDefault();
|
||||
|
||||
public override JsonNode? AsJson() => Pipeline?.AsJson();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.AppendLine($"FROM \"{Collection}\" PIPELINE {{");
|
||||
Pipeline?.Append(sb, indent + 1);
|
||||
sb.AppendLine("}");
|
||||
}
|
||||
|
||||
}
|
||||
42
Rms.Risk.Mango.Language/Ast/AstEquivalence.cs
Normal file
42
Rms.Risk.Mango.Language/Ast/AstEquivalence.cs
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstEquivalence : AstNodeBase
|
||||
{
|
||||
public AstEquivalence(AstExpressionVariable left, AstExpressionVariable right)
|
||||
{
|
||||
Add(left);
|
||||
Add(right);
|
||||
}
|
||||
|
||||
public AstExpressionVariable Left => Children.OfType<AstExpressionVariable>().First();
|
||||
public AstExpressionVariable Right => Children.OfType<AstExpressionVariable>().Last();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append(Spaces(indent));
|
||||
Left.Append(sb, indent + 1);
|
||||
sb.Append(" == ");
|
||||
Right.Append(sb, indent + 1);
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
24
Rms.Risk.Mango.Language/Ast/AstExpression.cs
Normal file
24
Rms.Risk.Mango.Language/Ast/AstExpression.cs
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public abstract class AstExpression : AstNodeBase
|
||||
{
|
||||
|
||||
}
|
||||
50
Rms.Risk.Mango.Language/Ast/AstExpressionArray.cs
Normal file
50
Rms.Risk.Mango.Language/Ast/AstExpressionArray.cs
Normal file
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionArray : AstExpression
|
||||
{
|
||||
public AstExpressionArray(IEnumerable<AstExpression> elements)
|
||||
{
|
||||
foreach( var v in elements)
|
||||
Add(v);
|
||||
}
|
||||
|
||||
public IReadOnlyList<AstExpression> Elements => Children.OfType<AstExpression>().ToList();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.AppendLine("[");
|
||||
var first = true;
|
||||
foreach (var v in Elements)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
sb.Append(Spaces(indent + 1));
|
||||
v.Append(sb, indent + 1);
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.Append($"{Spaces(indent)}]");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() => new JsonArray([.. Elements.Select(x => x.AsJson())]);
|
||||
}
|
||||
32
Rms.Risk.Mango.Language/Ast/AstExpressionBool.cs
Normal file
32
Rms.Risk.Mango.Language/Ast/AstExpressionBool.cs
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionBool(bool _value) : AstExpression
|
||||
{
|
||||
public bool Value => _value;
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append(Value.ToString().ToLower());
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() => JsonValue.Create(Value);
|
||||
}
|
||||
42
Rms.Risk.Mango.Language/Ast/AstExpressionBrackets.cs
Normal file
42
Rms.Risk.Mango.Language/Ast/AstExpressionBrackets.cs
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionBrackets : AstExpression
|
||||
{
|
||||
public AstExpressionBrackets() {}
|
||||
public AstExpressionBrackets(AstExpression expression)
|
||||
{
|
||||
Add(expression);
|
||||
}
|
||||
|
||||
public AstExpression Expression => Children.OfType<AstExpression>().First();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append("(");
|
||||
Expression.Append(sb, indent+1);
|
||||
sb.Append(")");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
return Expression.AsJson();
|
||||
}
|
||||
}
|
||||
41
Rms.Risk.Mango.Language/Ast/AstExpressionExists.cs
Normal file
41
Rms.Risk.Mango.Language/Ast/AstExpressionExists.cs
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionExists : AstExpression
|
||||
{
|
||||
public AstExpressionExists() {}
|
||||
public AstExpressionExists(string fieldName, bool exists)
|
||||
{
|
||||
Name = fieldName;
|
||||
Exists = exists;
|
||||
}
|
||||
|
||||
public string Name { get; internal set{ field = PreprocessFieldName(value); }} = "";
|
||||
public bool Exists { get; internal set;} = false;
|
||||
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
AppendField(sb, Name);
|
||||
sb.Append($" EXISTS");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() => new JsonObject([new("$exists", Exists)]);
|
||||
}
|
||||
107
Rms.Risk.Mango.Language/Ast/AstExpressionFunctionCall.cs
Normal file
107
Rms.Risk.Mango.Language/Ast/AstExpressionFunctionCall.cs
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionFunctionCall : AstExpression
|
||||
{
|
||||
public const string WithinFunctionProperty = "within_function";
|
||||
|
||||
public AstExpressionFunctionCall(string name, IEnumerable<AstFunctionArgument> namedArgs)
|
||||
{
|
||||
SetProperty(WithinFunctionProperty, true);
|
||||
Name = PreprocessFieldName(name);
|
||||
foreach (var arg in namedArgs)
|
||||
Add(arg);
|
||||
}
|
||||
|
||||
public string Name { get; internal set{ field = PreprocessFieldName(value); } }
|
||||
public IReadOnlyList<AstFunctionArgument> UnnamedArgs => Children.OfType<AstFunctionArgument>().Where(x => string.IsNullOrEmpty(x.Name)).ToList();
|
||||
public IReadOnlyList<AstFunctionArgument> NamedArgs => Children.OfType<AstFunctionArgument>().Where(x => !string.IsNullOrEmpty(x.Name)).ToList();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append($"{Name}( ");
|
||||
var namedArgs = NamedArgs;
|
||||
if ( namedArgs.Count == 0 )
|
||||
{
|
||||
// unnamed args
|
||||
bool first = true;
|
||||
foreach (var value in UnnamedArgs)
|
||||
{
|
||||
if ( !first )
|
||||
sb.Append(", ");
|
||||
else
|
||||
first = false;
|
||||
|
||||
value.Append(sb, indent+1);
|
||||
}
|
||||
sb.Append(" )");
|
||||
}
|
||||
else
|
||||
{
|
||||
// named args
|
||||
sb.AppendLine();
|
||||
bool first = true;
|
||||
foreach (var value in namedArgs)
|
||||
{
|
||||
if ( !first )
|
||||
{
|
||||
sb.AppendLine(", ");
|
||||
}
|
||||
else
|
||||
first = false;
|
||||
|
||||
sb.Append(Spaces(indent + 1));
|
||||
value.Append(sb, indent + 2);
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.Append($"{Spaces(indent)})");
|
||||
}
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
var namedArgs = NamedArgs;
|
||||
var unnamedArgs = UnnamedArgs;
|
||||
|
||||
if (namedArgs.Count == 0)
|
||||
{
|
||||
// no need for array if only one parameter
|
||||
if ( unnamedArgs.Count == 1 )
|
||||
return new JsonObject(
|
||||
[
|
||||
new($"${Name}", unnamedArgs[0].AsJson())
|
||||
]);
|
||||
return new JsonObject(
|
||||
[
|
||||
new(
|
||||
$"${Name}",
|
||||
new JsonArray([.. unnamedArgs.Select(x => x.AsJson())]))
|
||||
]);
|
||||
}
|
||||
|
||||
return new JsonObject(
|
||||
[
|
||||
new(
|
||||
$"${Name}",
|
||||
new JsonObject([.. namedArgs.Select(x => new KeyValuePair<string, JsonNode?>(x.Name, x.Value.AsJson()))])
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
69
Rms.Risk.Mango.Language/Ast/AstExpressionIn.cs
Normal file
69
Rms.Risk.Mango.Language/Ast/AstExpressionIn.cs
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionIn : AstExpression
|
||||
{
|
||||
private string _variable;
|
||||
private bool _not;
|
||||
|
||||
public AstExpressionIn(string variable, bool not, IEnumerable<AstExpression> values)
|
||||
{
|
||||
_variable = variable;
|
||||
_not = not;
|
||||
foreach (var value in values)
|
||||
Add(value);
|
||||
}
|
||||
|
||||
public string Variable => _variable;
|
||||
public bool Not => _not;
|
||||
public IReadOnlyList<AstExpression> Values => [.. Children.OfType<AstExpression>()];
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append($"{Variable} IN ( ");
|
||||
bool first = true;
|
||||
foreach (var value in Values)
|
||||
{
|
||||
if ( !first )
|
||||
sb.Append(", ");
|
||||
else
|
||||
first = false;
|
||||
|
||||
value.Append(sb, indent);
|
||||
}
|
||||
sb.Append($" )");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() =>
|
||||
new JsonObject(
|
||||
[
|
||||
new(
|
||||
"$in",
|
||||
new JsonArray
|
||||
{
|
||||
new JsonNode?[]{ JsonValue.Create(Variable) }
|
||||
.Concat(Values.Select(x => x.AsJson())
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
}
|
||||
29
Rms.Risk.Mango.Language/Ast/AstExpressionNull.cs
Normal file
29
Rms.Risk.Mango.Language/Ast/AstExpressionNull.cs
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionNull : AstExpression
|
||||
{
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append("NULL");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() => null; //JsonValue.Create("$null");
|
||||
}
|
||||
67
Rms.Risk.Mango.Language/Ast/AstExpressionNumber.cs
Normal file
67
Rms.Risk.Mango.Language/Ast/AstExpressionNumber.cs
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionNumber : AstExpression
|
||||
{
|
||||
public AstExpressionNumber(string value)
|
||||
{
|
||||
var d = double.Parse(value);
|
||||
if ( d == Math.Floor(d) )
|
||||
{
|
||||
LongValue = (long)d;
|
||||
IsLong = true;
|
||||
}
|
||||
else
|
||||
DoubleValue = d;
|
||||
}
|
||||
|
||||
|
||||
public AstExpressionNumber(long value)
|
||||
{
|
||||
LongValue = value;
|
||||
IsLong = true;
|
||||
}
|
||||
|
||||
public AstExpressionNumber(double value)
|
||||
{
|
||||
DoubleValue = value;
|
||||
}
|
||||
|
||||
public bool IsLong { get;}
|
||||
|
||||
public long LongValue { get; }
|
||||
public double DoubleValue { get; }
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
if (IsLong)
|
||||
sb.Append(LongValue);
|
||||
else
|
||||
sb.Append(DoubleValue);
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
if (IsLong)
|
||||
return JsonValue.Create(LongValue);
|
||||
else
|
||||
return JsonValue.Create(DoubleValue);
|
||||
}
|
||||
}
|
||||
290
Rms.Risk.Mango.Language/Ast/AstExpressionOperation.cs
Normal file
290
Rms.Risk.Mango.Language/Ast/AstExpressionOperation.cs
Normal file
@ -0,0 +1,290 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionOperation : AstExpression
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum OperationType
|
||||
{
|
||||
AND,
|
||||
OR,
|
||||
EQ,
|
||||
NEQ,
|
||||
GT,
|
||||
GTE,
|
||||
LT,
|
||||
LTE,
|
||||
PLUS,
|
||||
MINUS,
|
||||
DIVIDE,
|
||||
MULTIPLY
|
||||
}
|
||||
|
||||
public static OperationType GetOperationType(string op) => op switch
|
||||
{
|
||||
"$and" => OperationType.AND,
|
||||
"AND" => OperationType.AND,
|
||||
"$or" => OperationType.OR,
|
||||
"OR" => OperationType.OR,
|
||||
"$eq" => OperationType.EQ,
|
||||
"==" => OperationType.EQ,
|
||||
"$ne" => OperationType.NEQ,
|
||||
"<>" => OperationType.NEQ,
|
||||
"!=" => OperationType.NEQ,
|
||||
"$gt" => OperationType.GT,
|
||||
">" => OperationType.GT,
|
||||
"$gte" => OperationType.GTE,
|
||||
">=" => OperationType.GTE,
|
||||
"$lt" => OperationType.LT,
|
||||
"<" => OperationType.LT,
|
||||
"$lte" => OperationType.LTE,
|
||||
"<=" => OperationType.LTE,
|
||||
"$add" => OperationType.PLUS,
|
||||
"+" => OperationType.PLUS,
|
||||
"$subtract" => OperationType.MINUS,
|
||||
"-" => OperationType.MINUS,
|
||||
"$divide" => OperationType.DIVIDE,
|
||||
"/" => OperationType.DIVIDE,
|
||||
"$multiply" => OperationType.MULTIPLY,
|
||||
"*" => OperationType.MULTIPLY,
|
||||
_ => throw new($"Invalid operator '{op}'")
|
||||
};
|
||||
|
||||
public static string GetOperationStr(OperationType op) => op switch
|
||||
{
|
||||
OperationType.AND => "AND",
|
||||
OperationType.OR => "OR",
|
||||
OperationType.EQ => "==",
|
||||
OperationType.NEQ => "!=",
|
||||
OperationType.GT => ">",
|
||||
OperationType.GTE => ">=",
|
||||
OperationType.LT => "<",
|
||||
OperationType.LTE => "<=",
|
||||
OperationType.PLUS => "+",
|
||||
OperationType.MINUS => "-",
|
||||
OperationType.DIVIDE => "/",
|
||||
OperationType.MULTIPLY => "*",
|
||||
_ => throw new($"Invalid operator '{op}'")
|
||||
};
|
||||
|
||||
public static string GetOperationCall(OperationType op) => op switch
|
||||
{
|
||||
OperationType.AND => "$and",
|
||||
OperationType.OR => "$or",
|
||||
OperationType.EQ => "$eq",
|
||||
OperationType.NEQ => "$ne",
|
||||
OperationType.GT => "$gt",
|
||||
OperationType.GTE => "$gte",
|
||||
OperationType.LT => "$lt",
|
||||
OperationType.LTE => "$lte",
|
||||
OperationType.PLUS => "$add",
|
||||
OperationType.MINUS => "$subtract",
|
||||
OperationType.DIVIDE => "$divide",
|
||||
OperationType.MULTIPLY => "$multiply",
|
||||
_ => throw new($"Invalid operator '{op}'")
|
||||
};
|
||||
|
||||
private static HashSet<OperationType> _condition =
|
||||
[
|
||||
OperationType.EQ,
|
||||
OperationType.NEQ,
|
||||
OperationType.LT,
|
||||
OperationType.LTE,
|
||||
OperationType.GT,
|
||||
OperationType.GTE
|
||||
];
|
||||
|
||||
private static HashSet<OperationType> _groupable =
|
||||
[
|
||||
OperationType.AND,
|
||||
OperationType.OR,
|
||||
OperationType.PLUS,
|
||||
OperationType.MULTIPLY
|
||||
];
|
||||
|
||||
|
||||
public AstExpressionOperation(string op, params IEnumerable<AstExpression> args)
|
||||
: this(GetOperationType(op), args)
|
||||
{
|
||||
}
|
||||
|
||||
public AstExpressionOperation(OperationType op, params IEnumerable<AstExpression> args)
|
||||
{
|
||||
Operator = op;
|
||||
foreach (var arg in args)
|
||||
Add(arg);
|
||||
}
|
||||
|
||||
public OperationType Operator { get; }
|
||||
public string OperatorStr => GetOperationStr(Operator);
|
||||
public IReadOnlyList<AstExpression> Args => Children.OfType<AstExpression>().ToList();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
var first = true;
|
||||
foreach (var arg in Args)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Operator == OperationType.AND || Operator == OperationType.OR)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.Append($"{Spaces(indent)}{OperatorStr} ");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($" {OperatorStr} ");
|
||||
}
|
||||
}
|
||||
arg.Append(sb, indent);
|
||||
}
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
var functionArgs = Args;
|
||||
var withinFunction = GetProperty(AstExpressionFunctionCall.WithinFunctionProperty, false);
|
||||
// special case - equality operator
|
||||
if (_condition.Contains(Operator) && !withinFunction)
|
||||
{
|
||||
// for root level = use this form: { field : value }
|
||||
if (Operator == OperationType.EQ
|
||||
&& functionArgs is [
|
||||
AstExpressionVariable varArg,
|
||||
{ } condArg and (AstExpressionNumber or AstExpressionString or AstExpressionVariable)
|
||||
]
|
||||
)
|
||||
{
|
||||
var op = new JsonObject(
|
||||
[
|
||||
new(
|
||||
varArg.Name,
|
||||
condArg.AsJson()
|
||||
)
|
||||
]);
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
if (Operator == OperationType.EQ
|
||||
&& functionArgs is [
|
||||
{ } condArg1 and (AstExpressionNumber or AstExpressionString or AstExpressionVariable),
|
||||
AstExpressionVariable varArg1
|
||||
]
|
||||
)
|
||||
{
|
||||
var op = new JsonObject(
|
||||
[
|
||||
new(
|
||||
varArg1.Name,
|
||||
condArg1.AsJson()
|
||||
)
|
||||
]);
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
if (functionArgs is
|
||||
[
|
||||
AstExpressionVariable varArg2,
|
||||
{ } condArg2
|
||||
])
|
||||
{
|
||||
// for other conditions use { field : { $op : value } }
|
||||
var op1 = new JsonObject(
|
||||
[
|
||||
new(
|
||||
varArg2.Name,
|
||||
new JsonObject([
|
||||
new(
|
||||
GetOperationCall(Operator),
|
||||
condArg2.AsJson()
|
||||
)
|
||||
])
|
||||
)
|
||||
]
|
||||
);
|
||||
return op1;
|
||||
}
|
||||
}
|
||||
|
||||
var funcArgs = Args;
|
||||
|
||||
// make one for expressions like a+b+c+d
|
||||
if (_groupable.Contains(Operator))
|
||||
{
|
||||
var op = CreateFunctionCall(funcArgs);
|
||||
return op;
|
||||
}
|
||||
|
||||
// make separate calls for expressions like a-b-c-d
|
||||
var revArgs = funcArgs.Reverse().ToList();
|
||||
var currentFunc = CreateFunctionCall(revArgs[1], revArgs[0]);
|
||||
for (var i = 2; i < revArgs.Count; i++)
|
||||
currentFunc = ChainFunctionCall(revArgs[i], currentFunc);
|
||||
|
||||
return currentFunc;
|
||||
|
||||
JsonObject CreateFunctionCall(params IReadOnlyList<AstExpression> a)
|
||||
{
|
||||
var args = new JsonArray();
|
||||
foreach (var v in a.Select(x => x.AsJson()))
|
||||
{
|
||||
args.Add(v);
|
||||
}
|
||||
|
||||
var op = new JsonObject(
|
||||
[
|
||||
new(
|
||||
GetOperationCall(Operator),
|
||||
args
|
||||
)
|
||||
]
|
||||
);
|
||||
return op;
|
||||
}
|
||||
|
||||
JsonObject ChainFunctionCall(AstExpression arg1, JsonObject arg2)
|
||||
{
|
||||
JsonArray args =
|
||||
[
|
||||
arg1.AsJson(),
|
||||
arg2
|
||||
];
|
||||
|
||||
var op = new JsonObject(
|
||||
[
|
||||
new(
|
||||
GetOperationCall(Operator),
|
||||
args
|
||||
)
|
||||
]
|
||||
);
|
||||
return op;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
39
Rms.Risk.Mango.Language/Ast/AstExpressionProjection.cs
Normal file
39
Rms.Risk.Mango.Language/Ast/AstExpressionProjection.cs
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionProjection : AstExpression
|
||||
{
|
||||
public AstExpressionProjection() {}
|
||||
public AstExpressionProjection(string fieldName, JsonNode json )
|
||||
{
|
||||
Name = fieldName;
|
||||
Projection = json;
|
||||
}
|
||||
|
||||
public string Name { get; internal set{ field = PreprocessFieldName(value); }} = "";
|
||||
public JsonNode Projection { get; internal set;} = JsonValue.Create("<not initialized>");
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append($"{Name} IS {Projection.ToJsonString()}");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() => new JsonObject([new(Name, Projection)]);
|
||||
}
|
||||
33
Rms.Risk.Mango.Language/Ast/AstExpressionString.cs
Normal file
33
Rms.Risk.Mango.Language/Ast/AstExpressionString.cs
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionString(string _value) : AstExpression
|
||||
{
|
||||
public string Value => _value.Trim('\"');
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append($"\"{Value}\"");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() => JsonValue.Create(Value);
|
||||
|
||||
}
|
||||
107
Rms.Risk.Mango.Language/Ast/AstExpressionUnary.cs
Normal file
107
Rms.Risk.Mango.Language/Ast/AstExpressionUnary.cs
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstExpressionUnary : AstExpression
|
||||
{
|
||||
public enum OperationType
|
||||
{
|
||||
NOT,
|
||||
MINUS,
|
||||
PLUS
|
||||
|
||||
}
|
||||
|
||||
public AstExpressionUnary(OperationType op, AstExpression arg1)
|
||||
{
|
||||
Operator = op;
|
||||
Add(arg1);
|
||||
}
|
||||
|
||||
public OperationType Operator { get; }
|
||||
public string OperatorStr => Operator switch
|
||||
{
|
||||
OperationType.NOT => "NOT",
|
||||
OperationType.MINUS => "-",
|
||||
OperationType.PLUS => "+",
|
||||
_ => throw new($"Invalid operator '{Operator}'")
|
||||
};
|
||||
|
||||
public AstExpression Arg1 => Children.OfType<AstExpression>().First();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
switch (Operator)
|
||||
{
|
||||
case OperationType.MINUS:
|
||||
sb.Append("- ");
|
||||
break;
|
||||
case OperationType.PLUS:
|
||||
sb.Append("+ ");
|
||||
break;
|
||||
case OperationType.NOT:
|
||||
sb.Append("NOT ");
|
||||
break;
|
||||
}
|
||||
Arg1.Append(sb, indent);
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
switch (Operator)
|
||||
{
|
||||
case OperationType.PLUS:
|
||||
return Arg1.AsJson();
|
||||
case OperationType.MINUS:
|
||||
{
|
||||
var left = JsonValue.Create(0);
|
||||
var right = Arg1.AsJson();
|
||||
|
||||
var op = new JsonObject(
|
||||
[
|
||||
new(
|
||||
"$subtract",
|
||||
new JsonArray { new []{left,right} }
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
return op;
|
||||
|
||||
}
|
||||
case OperationType.NOT:
|
||||
{
|
||||
var right = Arg1.AsJson();
|
||||
var op = new JsonObject(
|
||||
[
|
||||
new(
|
||||
"$not",
|
||||
new JsonArray { new []{right} }
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
return op;
|
||||
}
|
||||
default:
|
||||
throw new($"Invalid operator '{Operator}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Rms.Risk.Mango.Language/Ast/AstExpressionVariable.cs
Normal file
30
Rms.Risk.Mango.Language/Ast/AstExpressionVariable.cs
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public partial class AstExpressionVariable(string _name) : AstExpression
|
||||
{
|
||||
public string Name => PreprocessFieldName(_name);
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
=>AppendField(sb, Name);
|
||||
|
||||
public override JsonNode? AsJson() => JsonValue.Create($"${Name}");
|
||||
|
||||
}
|
||||
47
Rms.Risk.Mango.Language/Ast/AstFunctionArgument.cs
Normal file
47
Rms.Risk.Mango.Language/Ast/AstFunctionArgument.cs
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstFunctionArgument : AstExpression
|
||||
{
|
||||
public AstFunctionArgument()
|
||||
{
|
||||
}
|
||||
|
||||
public AstFunctionArgument(string? name, AstExpression value)
|
||||
{
|
||||
Name = name!;
|
||||
Add(value);
|
||||
}
|
||||
|
||||
public string Name { get; internal set{ field = PreprocessFieldName(value); } } = "";
|
||||
public AstExpression Value => Children.OfType<AstExpression>().First();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Name))
|
||||
sb.Append($"{Name}: ");
|
||||
Value.Append(sb, indent);
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
=> string.IsNullOrWhiteSpace(Name)
|
||||
? Value.AsJson()
|
||||
: new JsonObject([new KeyValuePair<string, JsonNode?>(Name, Value.AsJson())]);
|
||||
}
|
||||
44
Rms.Risk.Mango.Language/Ast/AstLet.cs
Normal file
44
Rms.Risk.Mango.Language/Ast/AstLet.cs
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public abstract class AstLet : AstNodeBase
|
||||
{
|
||||
public AstLet(string? name = null)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string? Name { get; internal set{ field = PreprocessFieldName(value); } }
|
||||
public abstract void AddToJson(JsonObject res, bool simplifyTargetNames = false);
|
||||
public abstract void AddToJson(JsonArray res, bool simplifyTargetNames = false);
|
||||
|
||||
public static JsonObject AsJson(IEnumerable<AstLet> lets, bool simplifyTargetNames = false)
|
||||
{
|
||||
var res = new JsonObject();
|
||||
|
||||
foreach (var field in lets)
|
||||
field.AddToJson(res);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public sealed override JsonNode? AsJson()
|
||||
=> throw new NotImplementedException("Use AddToJson instead");
|
||||
}
|
||||
130
Rms.Risk.Mango.Language/Ast/AstLetArray.cs
Normal file
130
Rms.Risk.Mango.Language/Ast/AstLetArray.cs
Normal file
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstLetArray : AstLet
|
||||
{
|
||||
public AstLetArray()
|
||||
{
|
||||
}
|
||||
|
||||
public AstLetArray(string? name, IEnumerable<AstLet> fields, bool isArray)
|
||||
{
|
||||
Name = name?.Trim('\"');//PreprocessFieldName(name);
|
||||
IsArray = isArray;
|
||||
|
||||
foreach (var field in fields)
|
||||
Add(field);
|
||||
}
|
||||
|
||||
public bool IsArray { get; internal set; }
|
||||
|
||||
public IReadOnlyList<AstLet> Fields => Children.OfType<AstLet>().ToList();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
var openingBracket = IsArray ? "[" : "{";
|
||||
var closingBracket = IsArray ? "]" : "}";
|
||||
|
||||
sb.AppendLine($"{Spaces(indent)}{openingBracket}");
|
||||
var first = true;
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
field.Append(sb, indent + 1);
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
if ( !string.IsNullOrWhiteSpace(Name) )
|
||||
{
|
||||
sb.Append($"{Spaces(indent)}{closingBracket} AS ");
|
||||
AppendFieldName(sb, Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($"{Spaces(indent)}{closingBracket}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void AddToJson(JsonObject res, bool simplifyTargetNames = false)
|
||||
{
|
||||
if ( string.IsNullOrWhiteSpace(Name))
|
||||
throw new ("Array name must be specified");
|
||||
|
||||
var body = new JsonArray();
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
field.AddToJson(body, simplifyTargetNames);
|
||||
}
|
||||
|
||||
res.Add(Name!, body);
|
||||
}
|
||||
|
||||
public override void AddToJson(JsonArray res, bool simplifyTargetNames = false)
|
||||
{
|
||||
if (IsArray)
|
||||
{
|
||||
var body = new JsonArray();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
field.AddToJson(body, simplifyTargetNames);
|
||||
}
|
||||
|
||||
res.Add(new JsonObject([new(Name, body)]));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
field.AddToJson(body, simplifyTargetNames);
|
||||
}
|
||||
res.Add(body);
|
||||
}
|
||||
else
|
||||
{
|
||||
// this is an object
|
||||
var body = new JsonObject();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
field.AddToJson(body, simplifyTargetNames);
|
||||
}
|
||||
|
||||
res.Add(new JsonObject([new(Name, body)]));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
field.AddToJson(body, simplifyTargetNames);
|
||||
}
|
||||
res.Add(body);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
75
Rms.Risk.Mango.Language/Ast/AstLetExpression.cs
Normal file
75
Rms.Risk.Mango.Language/Ast/AstLetExpression.cs
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstLetExpression : AstLet
|
||||
{
|
||||
public AstLetExpression(AstExpression expression, string? name = null)
|
||||
{
|
||||
Add(expression);
|
||||
if ( expression is not AstExpressionVariable ev || ev.Name != PreprocessFieldName(name))
|
||||
{
|
||||
Name = name?.Trim('\"').Trim('\'');//PreprocessFieldName(name);
|
||||
if ( string.IsNullOrWhiteSpace(Name))
|
||||
Name = null;
|
||||
}
|
||||
}
|
||||
|
||||
public AstExpression Expression => Children.OfType<AstExpression>().First();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append(Spaces(indent));
|
||||
Expression.Append(sb, indent);
|
||||
|
||||
if ( !string.IsNullOrWhiteSpace(Name) )
|
||||
{
|
||||
sb.Append(" AS ");
|
||||
AppendFieldName(sb, Name);
|
||||
}
|
||||
}
|
||||
|
||||
public override void AddToJson(JsonObject res, bool simplifyTargetNames = false)
|
||||
{
|
||||
if ( !string.IsNullOrWhiteSpace(Name) )
|
||||
res.Add(Name, Expression.AsJson());
|
||||
else
|
||||
{
|
||||
var name = PreprocessFieldName((Expression as AstExpressionVariable)?.Name);
|
||||
if ( string.IsNullOrWhiteSpace(name) )
|
||||
throw new ($"{Expression} must have a name");
|
||||
// res.Add(name, simplifyTargetNames ? JsonValue.Create(1) : JsonValue.Create($"${name}"));
|
||||
res.Add(name, JsonValue.Create($"${name}"));
|
||||
}
|
||||
}
|
||||
|
||||
public override void AddToJson(JsonArray res, bool simplifyTargetNames = false)
|
||||
{
|
||||
if ( !string.IsNullOrWhiteSpace(Name) )
|
||||
res.Add( new JsonObject([ new(Name, Expression.AsJson())]));
|
||||
else
|
||||
{
|
||||
var name = PreprocessFieldName((Expression as AstExpressionVariable)?.Name);
|
||||
if ( string.IsNullOrWhiteSpace(name) )
|
||||
throw new ($"{Expression} must have a name");
|
||||
// res.Add( new JsonObject([ new(name, simplifyTargetNames ? JsonValue.Create(1) : JsonValue.Create($"${name}"))]));
|
||||
res.Add( new JsonObject([ new(name, JsonValue.Create($"${name}"))]));
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Rms.Risk.Mango.Language/Ast/AstNamedPipeline.cs
Normal file
46
Rms.Risk.Mango.Language/Ast/AstNamedPipeline.cs
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstNamedPipeline : AstNodeBase
|
||||
{
|
||||
public AstNamedPipeline(string name, AstPipeline pipeline)
|
||||
{
|
||||
Name = name;
|
||||
Add(pipeline);
|
||||
}
|
||||
|
||||
public string Name { get; internal set => field = PreprocessFieldName(value); } = "";
|
||||
public AstPipeline? Pipeline => Children.OfType<AstPipeline>().FirstOrDefault();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
if (Pipeline == null)
|
||||
throw new($"Pipeline is mandatory: {Name}");
|
||||
|
||||
sb.Append($"{Spaces(indent)}");
|
||||
AppendField(sb, Name);
|
||||
sb.AppendLine(" PIPELINE {");
|
||||
Pipeline.Append(sb, indent + 1);
|
||||
sb.Append($"{Spaces(indent)}}}");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
=> new JsonObject() { new(Name, Pipeline?.AsJson() ?? throw new($"Pipeline is mandatory: {Name}")) };
|
||||
}
|
||||
100
Rms.Risk.Mango.Language/Ast/AstNodeBase.cs
Normal file
100
Rms.Risk.Mango.Language/Ast/AstNodeBase.cs
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public abstract partial class AstNodeBase
|
||||
{
|
||||
private readonly Lazy<List<AstNodeBase>> _children = new();
|
||||
private readonly Lazy<Dictionary<string, object>> _properties = new();
|
||||
|
||||
public bool Empty => Count == 0;
|
||||
public int Count => _children.IsValueCreated ? _children.Value.Count : 0;
|
||||
public IReadOnlyList<AstNodeBase> Children => _children.Value;
|
||||
public AstNodeBase? Parent { get; set; } = null;
|
||||
|
||||
|
||||
public void Add(AstNodeBase child)
|
||||
{
|
||||
_children.Value.Add(child);
|
||||
child.Parent = this;
|
||||
}
|
||||
|
||||
public void SetProperty(string name, object value) => _properties.Value[name] = value;
|
||||
|
||||
public T? GetProperty<T>(string name, T? defaultValue = default(T), bool checkParent = true )
|
||||
{
|
||||
if (_properties.IsValueCreated && _properties.Value.TryGetValue(name, out var v))
|
||||
return (T)v;
|
||||
|
||||
if (Parent == null || !checkParent)
|
||||
return defaultValue;
|
||||
|
||||
return Parent.GetProperty<T>(name);
|
||||
}
|
||||
|
||||
public override string ToString() => AsText();
|
||||
|
||||
public string AsText()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
Append(sb, 0);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public abstract void Append(StringBuilder sb, int indent);
|
||||
public abstract JsonNode? AsJson();
|
||||
|
||||
protected static string Spaces(int indent)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 0; i < indent; i++)
|
||||
sb.Append(" ");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
[GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_]+$")]
|
||||
private static partial Regex SimpleVarNameRegex();
|
||||
|
||||
[GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_.]+$")]
|
||||
private static partial Regex ComplexVarNameRegex();
|
||||
|
||||
protected static string PreprocessFieldName(string? name)
|
||||
=> name?.Trim('\"').Trim('\'').TrimStart('$') ?? "";
|
||||
|
||||
protected static void AppendField(StringBuilder sb, string name)
|
||||
{
|
||||
if ( SimpleVarNameRegex().IsMatch(name) )
|
||||
sb.Append(name);
|
||||
else if ( ComplexVarNameRegex().IsMatch(name) )
|
||||
sb.Append($"${name}");
|
||||
else
|
||||
sb.Append($"\'{name}'");
|
||||
}
|
||||
|
||||
protected static void AppendFieldName(StringBuilder sb, string name)
|
||||
{
|
||||
if ( SimpleVarNameRegex().IsMatch(name) )
|
||||
sb.Append(name);
|
||||
else
|
||||
sb.Append($"\"{name}\"");
|
||||
}
|
||||
|
||||
}
|
||||
46
Rms.Risk.Mango.Language/Ast/AstPipeline.cs
Normal file
46
Rms.Risk.Mango.Language/Ast/AstPipeline.cs
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstPipeline : AstNodeBase
|
||||
{
|
||||
public static readonly AstPipeline None = new();
|
||||
|
||||
public AstPipeline()
|
||||
{
|
||||
}
|
||||
|
||||
public AstPipeline(IEnumerable<AstStage> stages)
|
||||
{
|
||||
foreach (var stage in stages)
|
||||
Add(stage);
|
||||
}
|
||||
|
||||
public IReadOnlyList<AstStage> Stages => Children.OfType<AstStage>().ToList();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
foreach (var stage in Stages)
|
||||
stage.Append(sb, indent);
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() => new JsonArray([.. Stages.Select(x => x.AsJson())]);
|
||||
}
|
||||
49
Rms.Risk.Mango.Language/Ast/AstSortField.cs
Normal file
49
Rms.Risk.Mango.Language/Ast/AstSortField.cs
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstSortField : AstNodeBase
|
||||
{
|
||||
public enum SortOrder { Ascending, Descending }
|
||||
|
||||
public AstSortField(){}
|
||||
public AstSortField( string name, SortOrder order )
|
||||
{
|
||||
Name = name;
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public string Name { get; internal set{ field = PreprocessFieldName(value); } } = "";
|
||||
public SortOrder Order { get; internal set; } = SortOrder.Ascending;
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append(Spaces(indent));
|
||||
AppendField(sb, Name);
|
||||
|
||||
if ( Order != SortOrder.Ascending )
|
||||
sb.Append(" DESC");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
var val = JsonValue.Create(Order == SortOrder.Ascending ? 1 : -1);
|
||||
return new JsonObject([new(Name, val)]);
|
||||
}
|
||||
}
|
||||
40
Rms.Risk.Mango.Language/Ast/AstStage.cs
Normal file
40
Rms.Risk.Mango.Language/Ast/AstStage.cs
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public abstract class AstStage : AstNodeBase
|
||||
{
|
||||
public JsonObject? Options { get; internal set; }
|
||||
|
||||
protected JsonNode? ApplyOptions(JsonNode? json )
|
||||
{
|
||||
if ( json is not JsonObject jo || Options == null)
|
||||
return json;
|
||||
|
||||
// apply overrides from Options object to jo and return the result
|
||||
var stage = jo.ElementAt(0).Value as JsonObject;
|
||||
if (stage == null)
|
||||
return json;
|
||||
|
||||
foreach (var (key, value) in Options)
|
||||
stage![key] = value?.DeepClone();
|
||||
|
||||
return jo;
|
||||
}
|
||||
}
|
||||
72
Rms.Risk.Mango.Language/Ast/AstStageAddFields.cs
Normal file
72
Rms.Risk.Mango.Language/Ast/AstStageAddFields.cs
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageAddFields : AstStage
|
||||
{
|
||||
public AstStageAddFields(IEnumerable<AstLet> fields)
|
||||
{
|
||||
foreach (var field in fields)
|
||||
Add((AstNodeBase)field);
|
||||
}
|
||||
|
||||
public IReadOnlyList<AstLet> Fields => Children.OfType<AstLet>().ToList();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent)}ADD");
|
||||
|
||||
var first = true;
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
if ( field is AstLetExpression let)
|
||||
{
|
||||
if ( (let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name))
|
||||
{
|
||||
sb.Append($"{Spaces(indent + 1)}");
|
||||
ev.Append(sb, indent + 1);
|
||||
}
|
||||
else
|
||||
let.Append(sb, indent + 1);
|
||||
}
|
||||
else
|
||||
((AstNodeBase)field).Append(sb, indent + 1);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
|
||||
var fields = new JsonObject();
|
||||
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
field.AddToJson(fields);
|
||||
}
|
||||
|
||||
var stage = new JsonObject([ new("$addFields", fields)]);
|
||||
return ApplyOptions(stage);
|
||||
}
|
||||
}
|
||||
236
Rms.Risk.Mango.Language/Ast/AstStageBucket.cs
Normal file
236
Rms.Risk.Mango.Language/Ast/AstStageBucket.cs
Normal file
@ -0,0 +1,236 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Globalization;
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageBucket : AstStage
|
||||
{
|
||||
private readonly List<string> _buckets = [];
|
||||
public bool Auto { get; internal set; }
|
||||
public int NumberOfBuckets { get; internal set; }
|
||||
public string? Granularity { get; internal set; }
|
||||
|
||||
public IReadOnlyList<AstLet> Fields => Children.OfType<AstLet>().ToList();
|
||||
|
||||
public IReadOnlyList<string> Buckets => _buckets;
|
||||
|
||||
public void AddBucket(string bucket) => _buckets.Add(bucket);
|
||||
public void AddBucket(double bucket) => _buckets.Add(bucket.ToString(CultureInfo.InvariantCulture));
|
||||
public void AddBucket(long bucket) => _buckets.Add(bucket.ToString());
|
||||
|
||||
public string? DefaultBucket { get; internal set; }
|
||||
public AstExpression GroupBy { get; internal set; } = null!;
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
if (Auto)
|
||||
AppendAuto(sb, indent);
|
||||
else
|
||||
AppendPlain(sb, indent);
|
||||
}
|
||||
|
||||
private void AppendPlain(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent)}BUCKET");
|
||||
|
||||
sb.Append($"{Spaces(indent+1)}");
|
||||
GroupBy.Append(sb, indent+1);
|
||||
sb.AppendLine();
|
||||
|
||||
sb.Append($"{Spaces(indent+1)}BOUNDARIES ");
|
||||
|
||||
var first = true;
|
||||
foreach (var bucket in Buckets)
|
||||
{
|
||||
if ( first )
|
||||
first = false;
|
||||
else
|
||||
sb.Append(", ");
|
||||
|
||||
if (double.TryParse(bucket, out _))
|
||||
sb.Append(bucket);
|
||||
else
|
||||
sb.Append($"\"{bucket}\"");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(DefaultBucket))
|
||||
{
|
||||
sb.Append($"{Spaces(indent+1)}DEFAULT ");
|
||||
|
||||
if (double.TryParse(DefaultBucket, out _))
|
||||
sb.Append(DefaultBucket);
|
||||
else
|
||||
sb.Append($"\"{DefaultBucket}\"");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
var fields = Fields;
|
||||
if (fields.Count > 0)
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent + 1)}LET");
|
||||
first = true;
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if (!first)
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
if (field is AstLetExpression let)
|
||||
{
|
||||
if ((let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name))
|
||||
{
|
||||
sb.Append($"{Spaces(indent + 2)}");
|
||||
ev.Append(sb, indent + 2);
|
||||
}
|
||||
else
|
||||
let.Append(sb, indent + 2);
|
||||
}
|
||||
else
|
||||
((AstNodeBase)field).Append(sb, indent + 2);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendAuto(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent)}BUCKET AUTO");
|
||||
|
||||
sb.Append($"{Spaces(indent+1)}");
|
||||
GroupBy.Append(sb, indent+1);
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine($"{Spaces(indent+1)}BUCKETS {NumberOfBuckets}");
|
||||
if ( !string.IsNullOrWhiteSpace(Granularity) )
|
||||
sb.AppendLine($"{Spaces(indent+1)}GRANULARITY \"{Granularity}\"");
|
||||
|
||||
var fields = Fields;
|
||||
if (fields.Count > 0)
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent + 1)}LET");
|
||||
var first = true;
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if (!first)
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
if (field is AstLetExpression let)
|
||||
{
|
||||
if ((let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name))
|
||||
{
|
||||
sb.Append($"{Spaces(indent + 2)}");
|
||||
ev.Append(sb, indent + 2);
|
||||
}
|
||||
else
|
||||
let.Append(sb, indent + 2);
|
||||
}
|
||||
else
|
||||
((AstNodeBase)field).Append(sb, indent + 2);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
=> Auto
|
||||
? AsJsonAuto()
|
||||
: AsJsonPlain()
|
||||
;
|
||||
|
||||
public JsonNode? AsJsonPlain()
|
||||
{
|
||||
var body = new JsonObject
|
||||
{
|
||||
["groupBy"] = GroupBy.AsJson()
|
||||
};
|
||||
|
||||
var bounds = new JsonArray();
|
||||
foreach (var bucket in Buckets)
|
||||
{
|
||||
if (long.TryParse(bucket, out var l))
|
||||
bounds.Add(l);
|
||||
else if (double.TryParse(bucket, out var d))
|
||||
bounds.Add(d);
|
||||
else
|
||||
bounds.Add(bucket);
|
||||
|
||||
}
|
||||
body["boundaries"] = bounds;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(DefaultBucket))
|
||||
{
|
||||
if (long.TryParse(DefaultBucket, out var l))
|
||||
body["default"] = JsonValue.Create(l);
|
||||
else if (double.TryParse(DefaultBucket, out var d))
|
||||
body["default"] = JsonValue.Create(d);
|
||||
else
|
||||
body["default"] = JsonValue.Create(DefaultBucket);
|
||||
|
||||
}
|
||||
|
||||
var fields = Fields;
|
||||
if (fields.Count > 0)
|
||||
{
|
||||
var let = new JsonObject();
|
||||
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
field.AddToJson(let);
|
||||
}
|
||||
|
||||
body["output"] = let;
|
||||
}
|
||||
|
||||
var stage = new JsonObject([ new("$bucket", body)]);
|
||||
return ApplyOptions(stage);
|
||||
}
|
||||
|
||||
public JsonNode? AsJsonAuto()
|
||||
{
|
||||
var body = new JsonObject
|
||||
{
|
||||
["groupBy"] = GroupBy.AsJson(),
|
||||
["buckets"] = NumberOfBuckets
|
||||
};
|
||||
|
||||
if ( !string.IsNullOrWhiteSpace(Granularity) )
|
||||
body["granularity"] = Granularity;
|
||||
|
||||
var fields = Fields;
|
||||
if (fields.Count > 0)
|
||||
{
|
||||
var let = new JsonObject();
|
||||
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
field.AddToJson(let);
|
||||
}
|
||||
|
||||
body["output"] = let;
|
||||
}
|
||||
|
||||
var stage = new JsonObject([ new("$bucketAuto", body)]);
|
||||
return ApplyOptions(stage);
|
||||
}
|
||||
}
|
||||
55
Rms.Risk.Mango.Language/Ast/AstStageDo.cs
Normal file
55
Rms.Risk.Mango.Language/Ast/AstStageDo.cs
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageDo : AstStage
|
||||
{
|
||||
public AstStageDo()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public AstStageDo(JsonNode json)
|
||||
{
|
||||
Json = json;
|
||||
}
|
||||
|
||||
|
||||
public JsonNode? Json { get; set; }
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
if ( Json == null )
|
||||
return;
|
||||
|
||||
sb.AppendLine($"{Spaces(indent)}DO ");
|
||||
var json = JsonSerializer.Serialize(Json, PrettyPrint)
|
||||
.Replace("\r", "")
|
||||
.Split("\n");
|
||||
sb.AppendJoin("\n", json.Select( x => $"{Spaces(indent)}{x}"));
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() => Json;
|
||||
|
||||
private static JsonSerializerOptions PrettyPrint = new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
}
|
||||
64
Rms.Risk.Mango.Language/Ast/AstStageFacet.cs
Normal file
64
Rms.Risk.Mango.Language/Ast/AstStageFacet.cs
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageFacet : AstStage
|
||||
{
|
||||
public IReadOnlyList<AstNamedPipeline> Pipelines => Children.OfType<AstNamedPipeline>().ToList();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent)}FACET");
|
||||
|
||||
var first = true;
|
||||
foreach (var field in Pipelines)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
field.Append(sb, indent + 1);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
var stage = new JsonObject
|
||||
{
|
||||
{
|
||||
"$facet",
|
||||
new JsonObject
|
||||
{
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var body = (JsonObject)stage.ElementAt(0).Value!;
|
||||
|
||||
foreach (var pipeline in Pipelines)
|
||||
{
|
||||
body.Add(pipeline.Name, pipeline.Pipeline!.AsJson());
|
||||
}
|
||||
|
||||
return ApplyOptions(stage);
|
||||
}
|
||||
|
||||
}
|
||||
105
Rms.Risk.Mango.Language/Ast/AstStageGroupBy.cs
Normal file
105
Rms.Risk.Mango.Language/Ast/AstStageGroupBy.cs
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageGroupBy : AstStage
|
||||
{
|
||||
private List<AstLet> _id = [];
|
||||
|
||||
public AstStageGroupBy(IEnumerable<AstLet> id, IEnumerable<AstLet> fields)
|
||||
{
|
||||
foreach (var x in fields)
|
||||
Add(x);
|
||||
foreach (var x in id)
|
||||
{
|
||||
_id.Add(x);
|
||||
x.Parent = this;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddId(AstLet field) => _id.Add(field);
|
||||
|
||||
public IReadOnlyList<AstLet> Id => _id;
|
||||
public IReadOnlyList<AstLet> Fields => Children.OfType<AstLet>().ToList();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent)}GROUP BY");
|
||||
|
||||
var first = true;
|
||||
foreach (var field in Id)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
field.Append(sb, indent + 1);
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
first = true;
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
{
|
||||
first = false;
|
||||
sb.AppendLine($"{Spaces(indent+1)}LET");
|
||||
}
|
||||
field.Append(sb, indent + 2);
|
||||
}
|
||||
|
||||
if ( !first )
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
var body = new JsonObject();
|
||||
|
||||
var _id = Id.OfType<AstLetExpression>().ToList();
|
||||
// _id can be a single field or an expression
|
||||
if (_id.Count == 1 && string.IsNullOrWhiteSpace(_id[0].Name) )
|
||||
body.Add("_id", _id[0].Expression.AsJson());
|
||||
|
||||
// alternative method for object-style _id
|
||||
if (body.Count == 0)
|
||||
{
|
||||
var idFields = new JsonObject();
|
||||
foreach (var field in _id)
|
||||
field.AddToJson(idFields);
|
||||
body.Add("_id", idFields);
|
||||
}
|
||||
|
||||
var fields = Fields.OfType<AstLetExpression>().ToList();
|
||||
|
||||
foreach (var field in fields)
|
||||
field.AddToJson(body);
|
||||
|
||||
var stage = new JsonObject
|
||||
{
|
||||
{ "$group", body }
|
||||
};
|
||||
|
||||
return ApplyOptions(stage);
|
||||
}
|
||||
|
||||
}
|
||||
124
Rms.Risk.Mango.Language/Ast/AstStageJoin.cs
Normal file
124
Rms.Risk.Mango.Language/Ast/AstStageJoin.cs
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageJoin : AstStage
|
||||
{
|
||||
public AstStageJoin()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public AstStageJoin(string collection, string asField, IEnumerable<AstEquivalence> on, IEnumerable<AstLet> let, AstPipeline? pipeline = null)
|
||||
{
|
||||
Init(collection, asField, on, let, pipeline);
|
||||
}
|
||||
|
||||
public void Init(string collection, string asField, IEnumerable<AstEquivalence> on, IEnumerable<AstLet> let, AstPipeline? pipeline = null)
|
||||
{
|
||||
Collection = collection.Trim('\"');
|
||||
AsField = PreprocessFieldName(asField);
|
||||
foreach(var x in on)
|
||||
Add(x);
|
||||
foreach(var x in let)
|
||||
Add(x);
|
||||
if (pipeline != null)
|
||||
Add(pipeline);
|
||||
}
|
||||
|
||||
public string Collection { get; private set;} = "";
|
||||
public string AsField { get; private set; } = "";
|
||||
|
||||
public IReadOnlyList<AstEquivalence> On => Children.OfType<AstEquivalence>().ToList();
|
||||
public IReadOnlyList<AstLet> Let => Children.OfType<AstLet>().ToList();
|
||||
public AstPipeline? Pipeline => Children.OfType<AstPipeline>().FirstOrDefault();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.Append($"{Spaces(indent)}JOIN \"{Collection}\" AS ");
|
||||
AppendField(sb, AsField);
|
||||
sb.AppendLine($" ON");
|
||||
|
||||
var first = true;
|
||||
foreach (var field in On)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
field.Append(sb, indent + 1);
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
if ( Let.Count > 0 )
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent+1)}LET");
|
||||
foreach (var field in Let)
|
||||
{
|
||||
field.Append(sb, indent + 2);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if( Pipeline != null )
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent + 1)}PIPELINE {{");
|
||||
Pipeline.Append(sb, indent + 2);
|
||||
sb.AppendLine($"{Spaces(indent + 1)}}}");
|
||||
}
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
var eqv = On.First();
|
||||
|
||||
var stage = new JsonObject
|
||||
{
|
||||
{
|
||||
"$lookup",
|
||||
new JsonObject
|
||||
{
|
||||
{"from", Collection},
|
||||
{"localField", JsonValue.Create(eqv.Left.Name)},
|
||||
{"foreignField", JsonValue.Create(eqv.Right.Name)}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var body = (JsonObject)stage.ElementAt(0).Value!;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(AsField))
|
||||
{
|
||||
body.Add( new("as", JsonValue.Create(AsField)) );
|
||||
}
|
||||
|
||||
if ( Let?.Count > 0 )
|
||||
{
|
||||
var fields =AstLet.AsJson(Let);
|
||||
body.Add( new("let", fields) );
|
||||
}
|
||||
|
||||
if ( Pipeline != null )
|
||||
body.Add( new("pipeline", Pipeline.AsJson()) );
|
||||
|
||||
return ApplyOptions(stage);
|
||||
}
|
||||
|
||||
}
|
||||
131
Rms.Risk.Mango.Language/Ast/AstStageProject.cs
Normal file
131
Rms.Risk.Mango.Language/Ast/AstStageProject.cs
Normal file
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageProject : AstStage
|
||||
{
|
||||
public AstStageProject() {}
|
||||
|
||||
public AstStageProject(IEnumerable<AstLet> id, IEnumerable<AstLet> fields, bool exclude = false)
|
||||
{
|
||||
_id.AddRange(id);
|
||||
Exclude = exclude;
|
||||
foreach (var field in fields.OfType<AstNodeBase>())
|
||||
Add(field);
|
||||
}
|
||||
|
||||
public void AddId(AstLet field)
|
||||
{
|
||||
_id.Add(field);
|
||||
}
|
||||
|
||||
private List<AstLet> _id = [];
|
||||
|
||||
public IReadOnlyList<AstLet> IdFields => _id;
|
||||
public IReadOnlyList<AstLet> Fields => Children.OfType<AstLet>().ToList();
|
||||
|
||||
public bool Exclude { get; internal set; }
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
bool first;
|
||||
if ( Exclude || IdFields.Count == 0)
|
||||
sb.AppendLine($"{Spaces(indent)}PROJECT{(Exclude ? " EXCLUDE" : "")}");
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent)}PROJECT ID {{");
|
||||
first = true;
|
||||
foreach (var field in IdFields)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
field.Append(sb, indent + 2);
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"{Spaces(indent+1)}}}");
|
||||
}
|
||||
|
||||
first = true;
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
if ( field is AstLetExpression let)
|
||||
{
|
||||
if ( (let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name))
|
||||
{
|
||||
sb.Append($"{Spaces(indent + 1)}");
|
||||
ev.Append(sb, indent + 1);
|
||||
}
|
||||
else
|
||||
let.Append(sb, indent + 1);
|
||||
}
|
||||
else
|
||||
field.Append(sb, indent + 1);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
|
||||
var fields = new JsonObject();
|
||||
if ( IdFields.Count > 0 )
|
||||
{
|
||||
var idFields = new JsonObject();
|
||||
|
||||
foreach (var field in IdFields)
|
||||
{
|
||||
if ( field is AstLetExpression let)
|
||||
AddFields(let, idFields);
|
||||
}
|
||||
|
||||
fields.Add("_id", idFields);
|
||||
}
|
||||
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if ( field is AstLetExpression let)
|
||||
AddFields(let, fields);
|
||||
}
|
||||
|
||||
var stage = new JsonObject([ new("$project", fields)]);
|
||||
return ApplyOptions(stage);
|
||||
}
|
||||
|
||||
private void AddFields(AstLetExpression field, JsonObject fields)
|
||||
{
|
||||
var name = field.Name;
|
||||
if ( string.IsNullOrWhiteSpace(name) )
|
||||
name = (field.Expression as AstExpressionVariable)?.Name;
|
||||
|
||||
if ( string.IsNullOrWhiteSpace(name) )
|
||||
throw new ($"{field.Expression} must have a name");
|
||||
|
||||
if ( (field.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(field.Name))
|
||||
fields.Add(ev.Name, JsonValue.Create(Exclude ? 0 : 1));
|
||||
else
|
||||
fields.Add(name, field.Expression.AsJson());
|
||||
}
|
||||
}
|
||||
128
Rms.Risk.Mango.Language/Ast/AstStageReplace.cs
Normal file
128
Rms.Risk.Mango.Language/Ast/AstStageReplace.cs
Normal file
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageReplace : AstStage
|
||||
{
|
||||
public AstStageReplace() {}
|
||||
|
||||
public AstStageReplace(IEnumerable<AstLet> id, IEnumerable<AstLet> fields)
|
||||
{
|
||||
_id.AddRange(id);
|
||||
foreach (var field in fields.OfType<AstNodeBase>())
|
||||
Add(field);
|
||||
}
|
||||
|
||||
public void AddId(AstLet field)
|
||||
{
|
||||
_id.Add(field);
|
||||
}
|
||||
|
||||
private List<AstLet> _id = [];
|
||||
|
||||
public IReadOnlyList<AstLet> IdFields => _id;
|
||||
public IReadOnlyList<AstLet> Fields => Children.OfType<AstLet>().ToList();
|
||||
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
bool first;
|
||||
|
||||
if ( IdFields.Count == 0)
|
||||
sb.AppendLine($"{Spaces(indent)}REPLACE");
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent)}REPLACE ID {{");
|
||||
|
||||
first = true;
|
||||
foreach (var field in IdFields)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
field.Append(sb, indent + 2);
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"{Spaces(indent+1)}}}");
|
||||
}
|
||||
|
||||
first = true;
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
if ( field is AstLetExpression let)
|
||||
{
|
||||
if ( (let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name))
|
||||
{
|
||||
sb.Append($"{Spaces(indent + 1)}");
|
||||
ev.Append(sb, indent + 1);
|
||||
}
|
||||
else
|
||||
let.Append(sb, indent + 1);
|
||||
}
|
||||
else
|
||||
field.Append(sb, indent + 1);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
|
||||
var fields = new JsonObject();
|
||||
if ( IdFields.Count > 0 )
|
||||
{
|
||||
var idFields = new JsonObject();
|
||||
|
||||
foreach (var field in IdFields)
|
||||
{
|
||||
if ( field is AstLetExpression let)
|
||||
AddFields(let, idFields);
|
||||
}
|
||||
|
||||
fields.Add("_id", idFields);
|
||||
}
|
||||
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if ( field is AstLetExpression let)
|
||||
AddFields(let, fields);
|
||||
}
|
||||
|
||||
var stage = new JsonObject([ new("$replaceWith", fields)]);
|
||||
return ApplyOptions(stage);
|
||||
}
|
||||
|
||||
private void AddFields(AstLetExpression field, JsonObject fields)
|
||||
{
|
||||
var name = field.Name;
|
||||
if ( string.IsNullOrWhiteSpace(name) )
|
||||
name = (field.Expression as AstExpressionVariable)?.Name;
|
||||
|
||||
if ( string.IsNullOrWhiteSpace(name) )
|
||||
throw new ($"{field.Expression} must have a name");
|
||||
|
||||
fields.Add(name, field.Expression.AsJson());
|
||||
}
|
||||
}
|
||||
61
Rms.Risk.Mango.Language/Ast/AstStageSortBy.cs
Normal file
61
Rms.Risk.Mango.Language/Ast/AstStageSortBy.cs
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageSortBy : AstStage
|
||||
{
|
||||
public AstStageSortBy()
|
||||
{
|
||||
}
|
||||
|
||||
public AstStageSortBy(IEnumerable<AstSortField> fields)
|
||||
{
|
||||
foreach (var field in fields)
|
||||
Add(field);
|
||||
}
|
||||
|
||||
public IReadOnlyList<AstSortField> Fields => Children.OfType<AstSortField>().ToList();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent)}SORT BY");
|
||||
var first = true;
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
if ( !first )
|
||||
sb.AppendLine(",");
|
||||
else
|
||||
first = false;
|
||||
|
||||
sb.Append(Spaces(indent + 1));
|
||||
field.Append(sb, indent + 1);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
var fields = new JsonObject();
|
||||
foreach (var field in Fields)
|
||||
fields.Add(field.Name, JsonValue.Create(field.Order == AstSortField.SortOrder.Descending ? -1 : 1));
|
||||
|
||||
var stage = new JsonObject([ new("$sort", fields)]);
|
||||
return ApplyOptions(stage);
|
||||
}
|
||||
}
|
||||
69
Rms.Risk.Mango.Language/Ast/AstStageUnwind.cs
Normal file
69
Rms.Risk.Mango.Language/Ast/AstStageUnwind.cs
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageUnwind : AstStage
|
||||
{
|
||||
public AstStageUnwind() {}
|
||||
public AstStageUnwind(string fieldName, string? indexName = null)
|
||||
{
|
||||
Name = fieldName;
|
||||
Index = indexName!;
|
||||
}
|
||||
|
||||
public string Name { get; internal set { field = PreprocessFieldName(value); }} = "";
|
||||
public string? Index { get; internal set { field = PreprocessFieldName(value); }}
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Index))
|
||||
sb.AppendLine($"{Spaces(indent)}UNWIND {Name}");
|
||||
else
|
||||
sb.AppendLine($"{Spaces(indent)}UNWIND {Name} INDEX {Index}");
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson()
|
||||
{
|
||||
if ( string.IsNullOrWhiteSpace(Index) )
|
||||
{
|
||||
var res = new JsonObject
|
||||
{{
|
||||
"$unwind",
|
||||
new JsonObject
|
||||
{
|
||||
{ "path", JsonValue.Create($"${Name}") }
|
||||
}
|
||||
}};
|
||||
return ApplyOptions(res);
|
||||
}
|
||||
else
|
||||
{
|
||||
var res = new JsonObject
|
||||
{{
|
||||
"$unwind",
|
||||
new JsonObject
|
||||
{
|
||||
{ "path", JsonValue.Create($"${Name}") },
|
||||
{ "includeArrayIndex", JsonValue.Create(Index) }
|
||||
}
|
||||
}};
|
||||
return ApplyOptions(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Rms.Risk.Mango.Language/Ast/AstStageWhere.cs
Normal file
44
Rms.Risk.Mango.Language/Ast/AstStageWhere.cs
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Ast;
|
||||
|
||||
public class AstStageWhere : AstStage
|
||||
{
|
||||
public AstStageWhere()
|
||||
{
|
||||
}
|
||||
|
||||
public AstStageWhere(AstExpression expression)
|
||||
{
|
||||
Add(expression);
|
||||
}
|
||||
|
||||
public AstExpression Expression => Children.OfType<AstExpression>().First();
|
||||
|
||||
public override void Append(StringBuilder sb, int indent)
|
||||
{
|
||||
sb.AppendLine($"{Spaces(indent)}WHERE");
|
||||
sb.Append(Spaces(indent + 1));
|
||||
Expression.Append(sb, indent + 1);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
public override JsonNode? AsJson() => ApplyOptions(new JsonObject([ new("$match", Expression.AsJson())]));
|
||||
}
|
||||
83
Rms.Risk.Mango.Language/JsonGrammar.g4
Normal file
83
Rms.Risk.Mango.Language/JsonGrammar.g4
Normal file
@ -0,0 +1,83 @@
|
||||
//
|
||||
// dbMango
|
||||
//
|
||||
// Copyright 2025 Deutsche Bank AG
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
grammar JsonGrammar;
|
||||
|
||||
json
|
||||
: object
|
||||
| array
|
||||
;
|
||||
|
||||
value
|
||||
: object
|
||||
| array
|
||||
| 'true'
|
||||
| 'false'
|
||||
| 'null'
|
||||
| NUMBER
|
||||
| STRING
|
||||
| VARIABLE
|
||||
;
|
||||
|
||||
object
|
||||
: '{' pair (',' pair)* '}'
|
||||
| '{' '}'
|
||||
;
|
||||
|
||||
pair
|
||||
: object_name ':' value
|
||||
;
|
||||
|
||||
object_name
|
||||
: VARIABLE
|
||||
| STRING
|
||||
;
|
||||
|
||||
array
|
||||
: '[' value (',' value)* ']'
|
||||
| '[' ']'
|
||||
;
|
||||
|
||||
// ---------------------- LEXER ----------------------------
|
||||
|
||||
// Fragments
|
||||
fragment DIGIT : [0-9];
|
||||
fragment INT : '-'? DIGIT+;
|
||||
fragment EXPONENT : [Ee] [+-]? DIGIT+;
|
||||
fragment FLOAT : INT ('.' DIGIT+)? EXPONENT?;
|
||||
|
||||
//fragment VERY_COMPLEX_VAR_FRAGMENT : '\'' [^']+ '\'';
|
||||
|
||||
fragment VAR_FRAGMENT : ([a-zA-Z_] [a-zA-Z0-9_]*) | ('$' [a-zA-Z_] [a-zA-Z0-9_.]*) | ('\'' (ESC | ~['\\])* '\'');
|
||||
fragment STRING_FRAGMENT : ('"' (ESC | ~["\\])* '"') | ('""');
|
||||
fragment ESC : '\\' (["'\\/bfnrt] | UNICODE);
|
||||
fragment UNICODE : 'u' HEX HEX HEX HEX;
|
||||
fragment HEX : [0-9a-fA-F];
|
||||
|
||||
// Tokens
|
||||
|
||||
NUMBER : INT | FLOAT;
|
||||
STRING : STRING_FRAGMENT;
|
||||
VARIABLE : VAR_FRAGMENT;
|
||||
|
||||
WS : [ \t\r\n]+ -> skip;
|
||||
|
||||
|
||||
COMMENT
|
||||
: ('/*' .*? '*/' | '//' .*? '\n') -> skip
|
||||
;
|
||||
70
Rms.Risk.Mango.Language/LanguageParser.cs
Normal file
70
Rms.Risk.Mango.Language/LanguageParser.cs
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using Antlr4.Runtime;
|
||||
using Antlr4.Runtime.Tree;
|
||||
|
||||
namespace Rms.Risk.Mango.Language;
|
||||
|
||||
public static class LanguageParser
|
||||
{
|
||||
public static AstAggregation ParseScriptToAST(string input)
|
||||
{
|
||||
var str = new AntlrInputStream(input);
|
||||
var lexer = new MongoAggregationForHumansLexer(str);
|
||||
var tokens = new CommonTokenStream(lexer);
|
||||
var parser = new MongoAggregationForHumansParser(tokens);
|
||||
|
||||
parser.AddErrorListener(new ErrorListener<IToken>("parser"));
|
||||
lexer.AddErrorListener(new ErrorListener<int>("lexer"));
|
||||
|
||||
var tree = parser.file();
|
||||
|
||||
var astListener = new MongoGrammarListener();
|
||||
var walker = new ParseTreeWalker();
|
||||
|
||||
walker.Walk(astListener, tree);
|
||||
|
||||
return astListener.Aggregate ?? throw new ("No Aggregation parsed");
|
||||
}
|
||||
|
||||
public static AstAggregation ParseAggregationJsonToAST(string collection, string json)
|
||||
=> AggregationPipelineParser.Parse(collection, json);
|
||||
|
||||
public static AstAggregation ParseAggregationJsonToAST(string collection, JsonArray json)
|
||||
=> AggregationPipelineParser.Parse(collection, json);
|
||||
|
||||
public static void ParseJsonForFun(string input)
|
||||
{
|
||||
var str = new AntlrInputStream(input);
|
||||
var lexer = new JsonGrammarLexer(str);
|
||||
var tokens = new CommonTokenStream(lexer);
|
||||
var parser = new JsonGrammarParser(tokens);
|
||||
|
||||
parser.AddErrorListener(new ErrorListener<IToken>("parser"));
|
||||
lexer.AddErrorListener(new ErrorListener<int>("lexer"));
|
||||
|
||||
var tree = parser.json();
|
||||
|
||||
var astListener = new JsonGrammarBaseListener();
|
||||
var walker = new ParseTreeWalker();
|
||||
|
||||
walker.Walk(astListener, tree);
|
||||
}
|
||||
|
||||
}
|
||||
192
Rms.Risk.Mango.Language/MongoAggregationForHumans.g4
Normal file
192
Rms.Risk.Mango.Language/MongoAggregationForHumans.g4
Normal file
@ -0,0 +1,192 @@
|
||||
//
|
||||
// dbMango
|
||||
//
|
||||
// Copyright 2025 Deutsche Bank AG
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
grammar MongoAggregationForHumans;
|
||||
import JsonGrammar;
|
||||
|
||||
file
|
||||
: 'FROM' STRING pipeline_def
|
||||
;
|
||||
|
||||
pipeline_def
|
||||
: 'PIPELINE' '{' stages_list '}'
|
||||
;
|
||||
|
||||
stages_list
|
||||
: stage_def
|
||||
| stages_list stage_def
|
||||
;
|
||||
|
||||
stage_def
|
||||
: match_def ('OPTIONS' json)?
|
||||
| bucket_def ('OPTIONS' json)?
|
||||
| facet_def ('OPTIONS' json)?
|
||||
| addfields_def ('OPTIONS' json)?
|
||||
| project_def ('OPTIONS' json)?
|
||||
| group_by_def ('OPTIONS' json)?
|
||||
| sort_def ('OPTIONS' json)?
|
||||
| join_def ('OPTIONS' json)?
|
||||
| unwind_def ('OPTIONS' json)?
|
||||
| replace_def ('OPTIONS' json)?
|
||||
| do_def ('OPTIONS' json)?
|
||||
;
|
||||
|
||||
match_def: 'WHERE' expression
|
||||
;
|
||||
|
||||
bucket_def
|
||||
: 'BUCKET' expression 'BOUNDARIES' NUMBER (',' NUMBER)* ('DEFAULT' defaultBucket=(NUMBER | STRING))? ('LET' let_list)? # BucketPlain
|
||||
| 'BUCKET' 'AUTO' expression 'BUCKETS' NUMBER ('GRANULARITY' STRING)? ('LET' let_list)? # BucketAuto
|
||||
;
|
||||
|
||||
facet_def
|
||||
: 'FACET' VARIABLE pipeline_def (',' VARIABLE pipeline_def)*
|
||||
;
|
||||
|
||||
addfields_def
|
||||
: 'ADD' let_list
|
||||
;
|
||||
|
||||
project_def
|
||||
: 'PROJECT' ('ID' '{' id_list=let_list '}')? data_list=let_list # ProjectInclude
|
||||
| 'PROJECT' 'EXCLUDE' var_list # ProjectExclude
|
||||
;
|
||||
|
||||
replace_def
|
||||
: 'REPLACE' 'ID' '{' id_list=let_list '}' data_list=let_list
|
||||
;
|
||||
|
||||
|
||||
group_by_def: 'GROUP' 'BY' id_list=let_list ('LET' data_list=let_list)?
|
||||
;
|
||||
|
||||
sort_def: 'SORT' 'BY' sort_var_list
|
||||
;
|
||||
|
||||
join_def: 'JOIN' STRING 'AS' (VARIABLE | STRING) 'ON' equivalence_list ('LET' let_list )? (pipeline_def)?
|
||||
;
|
||||
|
||||
unwind_def: 'UNWIND' VARIABLE ('INDEX' VARIABLE)?
|
||||
;
|
||||
|
||||
do_def: 'DO' json
|
||||
;
|
||||
|
||||
equivalence_list
|
||||
: left=VARIABLE '==' right=VARIABLE #VarEquivalence
|
||||
| equivalence_list ',' equivalence_list #EquivalenceList
|
||||
;
|
||||
|
||||
sort_var_list
|
||||
: VARIABLE (ASC | DESC)?
|
||||
| sort_var_list ',' sort_var_list
|
||||
;
|
||||
|
||||
var_list
|
||||
: VARIABLE
|
||||
| var_list ',' var_list
|
||||
;
|
||||
|
||||
let_list
|
||||
: let_list_item
|
||||
| let_list ',' let_list
|
||||
;
|
||||
|
||||
let_list_item
|
||||
: expression ('AS' (VARIABLE | STRING))? # LetExpressionAs
|
||||
| '[' let_list ']' ('AS' (VARIABLE | STRING))? # LetArray
|
||||
| '{' let_list '}' ('AS' (VARIABLE | STRING))? # LetObject
|
||||
;
|
||||
|
||||
expression
|
||||
: comparizon_expression ( (AND | OR) comparizon_expression )*
|
||||
;
|
||||
|
||||
comparizon_expression
|
||||
: additive_expression ( (EQ | NEQ | LT | LTE | GT | GTE) additive_expression )*
|
||||
;
|
||||
|
||||
additive_expression
|
||||
: multiplicative_expression ( (PLUS | MINUS) multiplicative_expression )*
|
||||
;
|
||||
|
||||
multiplicative_expression
|
||||
: unary_expression ( (MUL | DIV) unary_expression )*
|
||||
;
|
||||
|
||||
unary_expression
|
||||
: (PLUS | MINUS | NOT) unary_expression # UnaryExpression
|
||||
| brackets_expression # PrimaryExpression
|
||||
;
|
||||
|
||||
brackets_expression
|
||||
: atom # AtomExpression
|
||||
| VARIABLE NOT? 'IN' '(' expression (',' expression)* ')' # InExpression
|
||||
| VARIABLE '(' (unnamed_args_list | named_args_list ) ')' # FuncExpression
|
||||
| VARIABLE 'IS' json # ProjectionExpression
|
||||
| VARIABLE NOT? 'EXISTS' # ExistsExpression
|
||||
| '(' expression ')' # BracketsExpression
|
||||
;
|
||||
|
||||
named_args_list
|
||||
: left=VARIABLE ':' expression
|
||||
| left=VARIABLE ':' expression_array
|
||||
| named_args_list ',' named_args_list
|
||||
;
|
||||
|
||||
unnamed_args_list
|
||||
: expression
|
||||
| expression_array
|
||||
| unnamed_args_list ',' unnamed_args_list
|
||||
;
|
||||
|
||||
expression_array
|
||||
: '[' expression_array_item (',' expression_array_item)* ']'
|
||||
| '[' ']'
|
||||
;
|
||||
|
||||
expression_array_item
|
||||
: expression
|
||||
| expression_array
|
||||
;
|
||||
|
||||
|
||||
atom
|
||||
: STRING
|
||||
| NUMBER
|
||||
| 'true'
|
||||
| 'false'
|
||||
| 'null'
|
||||
| VARIABLE
|
||||
;
|
||||
|
||||
AND: 'AND' | '&&';
|
||||
OR: 'OR' | '||';
|
||||
NOT: 'NOT' | '!';
|
||||
EQ: '==';
|
||||
NEQ: '<>' | '!=';
|
||||
GT: '>';
|
||||
GTE: '>=';
|
||||
LT: '<';
|
||||
LTE: '<=';
|
||||
ASC: 'ASC';
|
||||
DESC: 'DESC';
|
||||
MUL: '*';
|
||||
DIV: '/';
|
||||
PLUS: '+';
|
||||
MINUS: '-';
|
||||
685
Rms.Risk.Mango.Language/Parsers/AggregationPipelineParser.cs
Normal file
685
Rms.Risk.Mango.Language/Parsers/AggregationPipelineParser.cs
Normal file
@ -0,0 +1,685 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Parsers;
|
||||
|
||||
internal static class AggregationPipelineParser
|
||||
{
|
||||
public static AstAggregation Parse(string collection, string json)
|
||||
{
|
||||
var arr = (JsonArray?)JsonNode.Parse(json);
|
||||
if ( arr == null )
|
||||
throw new ("Json array expected");
|
||||
return Parse(collection, arr);
|
||||
}
|
||||
|
||||
public static AstAggregation Parse(string collection, JsonArray json)
|
||||
{
|
||||
var agg = new AstAggregation(collection);
|
||||
var pipeline = new AstPipeline();
|
||||
|
||||
foreach (var stageJson in json )
|
||||
{
|
||||
if ( stageJson is not JsonObject jo )
|
||||
throw new ("Json object expected as stage");
|
||||
|
||||
var stage = ParseStage(jo);
|
||||
pipeline.Add(stage);
|
||||
}
|
||||
|
||||
agg.Add(pipeline);
|
||||
return agg;
|
||||
}
|
||||
|
||||
private static AstStage ParseStage(JsonObject json)
|
||||
{
|
||||
var name = json.ElementAt(0).Key;
|
||||
|
||||
if (json.ElementAt(0).Value is JsonValue jv)
|
||||
{
|
||||
if (name == "$unwind")
|
||||
return ParseUnwind(jv);
|
||||
else
|
||||
return new AstStageDo(json);
|
||||
}
|
||||
|
||||
if (json.ElementAt(0).Value is not JsonObject body)
|
||||
throw new($"Json object expected as body of {name} stage");
|
||||
|
||||
switch (name)
|
||||
{
|
||||
case "$addFields": return ParseAddFields(body);
|
||||
case "$bucket": return ParseBucket(body);
|
||||
case "$bucketAuto": return ParseBucketAuto(body);
|
||||
case "$facet": return ParseFacet(body);
|
||||
case "$project": return ParseProject(body);
|
||||
case "$match": return ParseMatch(body);
|
||||
case "$group": return ParseGroup(body);
|
||||
case "$sort": return ParseSort(body);
|
||||
case "$unwind": return ParseUnwind(body);
|
||||
case "$lookup": return ParseLookup(body);
|
||||
case "$replaceWith": return ParseReplaceWith(body);
|
||||
// case "$merge": return ParseMerge(body);
|
||||
// case "$out": return ParseOut(body);
|
||||
// case "$limit": return ParseLimit(body);
|
||||
// case "$skip": return ParseSkip(body);
|
||||
// case "$count": return ParseCount(body);
|
||||
default:
|
||||
return new AstStageDo(json);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static AstStage ParseLookup(JsonObject body)
|
||||
{
|
||||
if ( !body.TryGetPropertyValue("from", out var collection)
|
||||
|| !body.TryGetPropertyValue("localField", out var local)
|
||||
|| !body.TryGetPropertyValue("foreignField", out var foreign)
|
||||
|| !body.TryGetPropertyValue("as", out var asField)
|
||||
)
|
||||
throw new($"Invalid lookup stage: {body.ToJsonString()}");
|
||||
|
||||
var eqv = new AstEquivalence( new(local!.GetValue<string>()), new(foreign!.GetValue<string>()) );
|
||||
return new AstStageJoin(collection!.GetValue<string>(), asField!.GetValue<string>(), [eqv], [], null);
|
||||
}
|
||||
|
||||
private static AstStage ParseReplaceRoot(JsonObject body)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static AstStage ParseUnwind(JsonObject body)
|
||||
{
|
||||
var path = body.ElementAt(0).Value?.GetValue<string>() ?? throw new($"Expected path: {body}");
|
||||
string? index = null;
|
||||
if (body.TryGetPropertyValue("includeArrayIndex", out var indexNode))
|
||||
index= indexNode!.GetValue<string>();
|
||||
|
||||
return new AstStageUnwind(path, index);
|
||||
}
|
||||
|
||||
private static AstStage ParseUnwind(JsonValue body)
|
||||
{
|
||||
var path = body.GetValue<string>() ?? throw new($"Expected path: {body}");
|
||||
return new AstStageUnwind(path);
|
||||
}
|
||||
|
||||
private static AstStage ParseMerge(JsonObject body)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static AstStage ParseSort(JsonObject body)
|
||||
{
|
||||
var order = new List<AstSortField>();
|
||||
foreach (var field in body)
|
||||
{
|
||||
var name = field.Key;
|
||||
var sortOrder = field.Value?.GetValue<int>() ?? 1;
|
||||
|
||||
order.Add(new(name, sortOrder != -1 ? AstSortField.SortOrder.Ascending : AstSortField.SortOrder.Descending));
|
||||
}
|
||||
return new AstStageSortBy(order);
|
||||
}
|
||||
|
||||
private static AstStage ParseGroup(JsonObject body)
|
||||
{
|
||||
var fields = new List<AstLet>();
|
||||
var id = new List<AstLet>();
|
||||
|
||||
foreach (var field in body)
|
||||
{
|
||||
if ( field.Key == "_id")
|
||||
{
|
||||
if (field.Value is JsonValue jv && jv.GetValueKind() == JsonValueKind.String)
|
||||
{
|
||||
var let = new AstLetExpression(new AstExpressionVariable(jv.GetValue<string>()));
|
||||
id.Add(let);
|
||||
}
|
||||
else
|
||||
{
|
||||
var idObj = field.Value as JsonObject ??
|
||||
throw new($"_id must be an object: {field.Value?.ToJsonString()}");
|
||||
|
||||
foreach (var idField in idObj)
|
||||
{
|
||||
var (let, _) = ParseLet(idField);
|
||||
id.Add(let);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var (let, _) = ParseLet(field);
|
||||
fields.Add(let);
|
||||
}
|
||||
}
|
||||
|
||||
var stage = new AstStageGroupBy(id, fields);
|
||||
return stage;
|
||||
}
|
||||
|
||||
private static AstStage ParseMatch(JsonObject body)
|
||||
{
|
||||
// special case: there is no logical function at the top
|
||||
if ( body.Count == 1 )
|
||||
{
|
||||
var field = body.ElementAt(0);
|
||||
if ( !field.Key.StartsWith("$") && field.Value is JsonObject jo )
|
||||
{
|
||||
var expr = ParseLogicalFuncArgument(jo);
|
||||
var eq = new AstExpressionOperation(AstExpressionOperation.OperationType.EQ, new AstExpressionVariable(field.Key), expr);
|
||||
return new AstStageWhere(eq);
|
||||
}
|
||||
}
|
||||
var expression = ParseExpression(body);
|
||||
return new AstStageWhere(expression);
|
||||
}
|
||||
|
||||
private static HashSet<string> _operations = [
|
||||
"$and",
|
||||
"$or",
|
||||
"$eq",
|
||||
"$ne",
|
||||
"$gt",
|
||||
"$gte",
|
||||
"$lt",
|
||||
"$lte",
|
||||
"$add",
|
||||
"$subtract",
|
||||
"$divide",
|
||||
"$multiply"
|
||||
];
|
||||
|
||||
private static HashSet<string> _projectionLogicalOperations = [
|
||||
"$eq",
|
||||
"$ne",
|
||||
"$gt",
|
||||
"$gte",
|
||||
"$lt",
|
||||
"$lte"
|
||||
];
|
||||
|
||||
|
||||
private static AstExpression ParseExpression(JsonNode? json)
|
||||
{
|
||||
switch (json)
|
||||
{
|
||||
case null: return new AstExpressionNull();
|
||||
case JsonArray ja:
|
||||
throw new ($"Unexpected array {ja}");
|
||||
case JsonObject jo:
|
||||
{
|
||||
if (_operations.Contains(jo.ElementAt(0).Key))
|
||||
return ParseOperation(jo);
|
||||
else
|
||||
return ParseFunctionCall(jo);
|
||||
}
|
||||
case JsonValue jv:
|
||||
{
|
||||
switch (jv.GetValueKind())
|
||||
{
|
||||
case JsonValueKind.String:
|
||||
if (jv.GetValue<string>().StartsWith('$') && !jv.GetValue<string>().StartsWith("$$"))
|
||||
return new AstExpressionVariable(jv.GetValue<string>());
|
||||
else
|
||||
return new AstExpressionString(jv.GetValue<string>());
|
||||
case JsonValueKind.Number:
|
||||
if (jv.TryGetValue<long>(out var l))
|
||||
return new AstExpressionNumber(l);
|
||||
if (jv.TryGetValue<int>(out var i))
|
||||
return new AstExpressionNumber((long)i);
|
||||
if (jv.TryGetValue<double>(out var d))
|
||||
return new AstExpressionNumber(d);
|
||||
throw new($"Invalid number {jv}");
|
||||
case JsonValueKind.True: return new AstExpressionBool(true);
|
||||
case JsonValueKind.False: return new AstExpressionBool(false);
|
||||
case JsonValueKind.Null: return new AstExpressionNull();
|
||||
default: throw new($"Invalid json value {jv}");
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new($"Invalid json expression {json}");
|
||||
}
|
||||
|
||||
private static AstExpressionOperation? TryParseProjectionOperation(JsonNode? json)
|
||||
{
|
||||
if ( json is not JsonObject jo )
|
||||
return null;
|
||||
|
||||
if ( jo.Count != 1 )
|
||||
return null;
|
||||
|
||||
var fieldName = jo.ElementAt(0).Key;
|
||||
if ( fieldName.StartsWith("$") )
|
||||
return null;
|
||||
|
||||
var operationFunc = jo.ElementAt(0).Value as JsonObject;
|
||||
if ( operationFunc?.Count != 1 )
|
||||
return null;
|
||||
|
||||
var operationName = operationFunc.ElementAt(0).Key;
|
||||
if ( !_projectionLogicalOperations.Contains(operationName) )
|
||||
return null;
|
||||
|
||||
// HACK: { field: { $gt: {} } is equivalent to { field: { $exists: true } }
|
||||
if ( operationName == "$gt" && operationFunc.ElementAt(0).Value is JsonObject { Count: 0 } )
|
||||
{
|
||||
return new(
|
||||
AstExpressionOperation.OperationType.EQ,
|
||||
new AstExpressionVariable(fieldName),
|
||||
new AstExpressionFunctionCall(
|
||||
"exists",
|
||||
[new (null, new AstExpressionBool(true))]));
|
||||
}
|
||||
|
||||
var operationParam = ParseExpression(operationFunc.ElementAt(0).Value);
|
||||
|
||||
return new(operationName, new AstExpressionVariable(fieldName), operationParam);
|
||||
}
|
||||
|
||||
private static AstExpression ParseOperation(JsonObject json)
|
||||
{
|
||||
var funcName = json.ElementAt(0).Key;
|
||||
if (!funcName.StartsWith("$"))
|
||||
throw new ($"Operation name {funcName} must start with $");
|
||||
|
||||
if ( json.ElementAt(0).Value is not JsonArray funcParams || funcParams.Count == 0 )
|
||||
throw new($"Operation {funcName} parameters must be an array: {json?.ToJsonString()}");
|
||||
|
||||
if ( funcName == "$and" || funcName == "$or")
|
||||
return ParseLogicalOperation(funcName, funcParams);
|
||||
|
||||
if ( funcParams.Count != 2 )
|
||||
throw new($"Operation {funcName} must have 2 parameters");
|
||||
|
||||
var arg1 = ParseExpression(funcParams[0]);
|
||||
var arg2 = ParseExpression(funcParams[1]);
|
||||
|
||||
return new AstExpressionOperation(funcName, arg1, arg2);
|
||||
}
|
||||
|
||||
private static AstExpression ParseLogicalOperation(string funcName, JsonArray funcParams)
|
||||
{
|
||||
var args = funcParams.Select(ParseLogicalFuncArgument).ToList();
|
||||
if ( args.Count == 1 )
|
||||
return args[0];
|
||||
if ( args.Count == 2)
|
||||
{
|
||||
var isLogical0 = (args[0] as AstExpressionOperation) is { Operator: AstExpressionOperation.OperationType.AND } or { Operator: AstExpressionOperation.OperationType.OR };
|
||||
var isLogical1 = (args[1] as AstExpressionOperation) is { Operator: AstExpressionOperation.OperationType.AND } or { Operator: AstExpressionOperation.OperationType.OR };
|
||||
|
||||
var arg0 = isLogical0 ? new AstExpressionBrackets(args[0]) : args[0];
|
||||
var arg1 = isLogical1 ? new AstExpressionBrackets(args[1]) : args[1];
|
||||
return new AstExpressionOperation(funcName, arg0, arg1);
|
||||
}
|
||||
return new AstExpressionBrackets(Join(funcName, args));
|
||||
}
|
||||
|
||||
private static AstExpressionOperation Join( string funcName, List<AstExpression> args)
|
||||
{
|
||||
if ( args.Count < 2 )
|
||||
throw new($"Expecting at least 3 parameters for joining {funcName} operations");
|
||||
if ( args.Count == 2 )
|
||||
return new(funcName, args[0], args[1]);
|
||||
|
||||
var op = new AstExpressionOperation(funcName, args[0], Join(funcName, [.. args.Skip(1)]));
|
||||
return op;
|
||||
}
|
||||
|
||||
private static AstExpressionFunctionCall ParseFunctionCall(JsonObject json)
|
||||
{
|
||||
var funcName = json.ElementAt(0).Key;
|
||||
if (!funcName.StartsWith("$"))
|
||||
throw new ($"Function name \"{funcName}\" must start with $: {json?.ToJsonString()}");
|
||||
|
||||
var funcParams = json.ElementAt(0).Value;
|
||||
|
||||
List<AstFunctionArgument> namedParams = [];
|
||||
List<AstFunctionArgument> unnamedParams = [];
|
||||
|
||||
if (funcParams is JsonValue jv)
|
||||
unnamedParams.Add(new (null, ParseExpression(jv)));
|
||||
if (funcParams is JsonArray ja)
|
||||
unnamedParams.AddRange(ja.Select(x => new AstFunctionArgument(null, ParseExpression(x))));
|
||||
if (funcParams is JsonObject jo)
|
||||
{
|
||||
foreach ( var arg in jo)
|
||||
{
|
||||
if ( arg.Value is JsonArray arrArg )
|
||||
{
|
||||
var arrayMembers = new List<AstExpression>();
|
||||
foreach (var member in arrArg)
|
||||
arrayMembers.Add(ParseExpression(member));
|
||||
|
||||
namedParams.Add(new(arg.Key, new AstExpressionArray(arrayMembers)));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (arg.Key.StartsWith("$") )
|
||||
{
|
||||
var funcObj = new JsonObject([new(arg.Key, arg.Value?.DeepClone())]);
|
||||
unnamedParams.Add(new(null, ParseFunctionCall(funcObj)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.Value is JsonObject inner)
|
||||
{
|
||||
if (inner.Count == 1 && inner.ElementAt(0).Key.StartsWith('$') )
|
||||
{
|
||||
//var funcObj = new JsonObject([new(inner.ElementAt(0).Key, inner.ElementAt(0).Value?.DeepClone())]);
|
||||
namedParams.Add(new(arg.Key, ParseExpression(inner)));
|
||||
continue;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var expr = ParseExpression(arg.Value);
|
||||
if ( expr is AstExpressionFunctionCall )
|
||||
unnamedParams.Add(new (null, expr));
|
||||
else
|
||||
namedParams.Add(new(arg.Key, expr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new(funcName, unnamedParams.Concat( namedParams ));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Special case for $and and $or - their arguments can be like { aaa : 1} which means "a == 1".
|
||||
/// </summary>
|
||||
private static AstExpression ParseLogicalFuncArgument(JsonNode? json)
|
||||
{
|
||||
if (json is JsonObject jo && !jo.ElementAt(0).Key.StartsWith('$'))
|
||||
{
|
||||
var projectionOperation = TryParseProjectionOperation(json);
|
||||
if ( projectionOperation != null )
|
||||
return projectionOperation;
|
||||
|
||||
if ( jo.ElementAt(0).Value is JsonValue )
|
||||
{
|
||||
// simple equality
|
||||
var right = ParseExpression(jo.ElementAt(0).Value);
|
||||
return new AstExpressionOperation(AstExpressionOperation.OperationType.EQ, new AstExpressionVariable(jo.ElementAt(0).Key), right);
|
||||
}
|
||||
else if ( jo.ElementAt(0).Value is JsonObject joRight )
|
||||
{
|
||||
// projection https://www.mongodb.com/docs/manual/reference/operator/query/
|
||||
// only $exists is supported.
|
||||
if (joRight.ElementAt(0).Key == "$exists" && joRight.ElementAt(0).Value is JsonValue)
|
||||
return new AstExpressionExists(jo.ElementAt(0).Key, joRight.ElementAt(0).Value!.GetValue<bool>());
|
||||
|
||||
// full range of projections is not supported.
|
||||
//return new AstExpressionProjection(jo.ElementAt(0).Key, joRight);
|
||||
var right = ParseExpression(jo.ElementAt(0).Value);
|
||||
return new AstExpressionOperation(AstExpressionOperation.OperationType.EQ, new AstExpressionVariable(jo.ElementAt(0).Key), right);
|
||||
}
|
||||
}
|
||||
return ParseExpression(json);
|
||||
}
|
||||
|
||||
private static AstStage ParseProject(JsonObject body)
|
||||
{
|
||||
var fields = new List<AstLet>();
|
||||
var idFields = new List<AstLet>();
|
||||
|
||||
AstLet? let;
|
||||
|
||||
var exclude = false;
|
||||
foreach (var field in body)
|
||||
{
|
||||
if ( field is { Key: "_id", Value: JsonObject idObj })
|
||||
{
|
||||
foreach (var idField in idObj)
|
||||
{
|
||||
(let, _) = ParseLet(idField);
|
||||
idFields.Add(let);
|
||||
}
|
||||
exclude = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool exc;
|
||||
|
||||
(let, exc) = ParseLet(field);
|
||||
exclude = exc;
|
||||
fields.Add(let);
|
||||
}
|
||||
|
||||
var stage = new AstStageProject(idFields, fields, exclude);
|
||||
return stage;
|
||||
}
|
||||
|
||||
private static AstStage ParseReplaceWith(JsonObject body)
|
||||
{
|
||||
var fields = new List<AstLet>();
|
||||
var idFields = new List<AstLet>();
|
||||
|
||||
AstLet? let;
|
||||
|
||||
foreach (var field in body)
|
||||
{
|
||||
if ( field is { Key: "_id", Value: JsonObject idObj })
|
||||
{
|
||||
foreach (var idField in idObj)
|
||||
{
|
||||
(let, _) = ParseLet(idField);
|
||||
idFields.Add(let);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
(let, _) = ParseLet(field);
|
||||
fields.Add(let);
|
||||
}
|
||||
|
||||
var stage = new AstStageReplace(idFields, fields);
|
||||
return stage;
|
||||
}
|
||||
|
||||
private static (AstLet, bool) ParseLet(KeyValuePair<string, JsonNode?> field)
|
||||
{
|
||||
var exclude = false;
|
||||
|
||||
if ( field.Value is JsonArray ja )
|
||||
return (ParseArrayProjection(field.Key, ja),false);
|
||||
if ( field.Value is JsonObject { Count: > 0 } jo && !jo.ElementAt(0).Key.StartsWith('$') )
|
||||
return (ParseObjectProjection(field.Key, jo),false);
|
||||
|
||||
var expression = ParseExpression(field.Value);
|
||||
AstLet let;
|
||||
|
||||
// if any argument is like aaa : 0 this means PROJECT EXCLUDE aaa
|
||||
if (expression is AstExpressionNumber { IsLong: true } en)
|
||||
{
|
||||
if (en.LongValue == 0)
|
||||
exclude = true;
|
||||
let = new AstLetExpression(new AstExpressionVariable(field.Key));
|
||||
}
|
||||
else
|
||||
let = new AstLetExpression(expression, field.Key);
|
||||
return (let, exclude);
|
||||
}
|
||||
|
||||
private static AstLetArray ParseArrayProjection(string name, JsonArray ja)
|
||||
{
|
||||
var fields = new List<AstLet>();
|
||||
|
||||
foreach ( var o in ja.OfType<JsonObject>() )
|
||||
{
|
||||
var singleLet = ParseLet(new("", o)).Item1;
|
||||
fields.Add(singleLet);
|
||||
}
|
||||
var res = new AstLetArray(name, fields, true);
|
||||
return res;
|
||||
}
|
||||
|
||||
private static AstLetArray ParseObjectProjection(string name, JsonObject jo)
|
||||
{
|
||||
var fields = new List<AstLet>();
|
||||
|
||||
foreach (var pair in jo)
|
||||
{
|
||||
var singleLet = ParseLet(pair).Item1;
|
||||
fields.Add(singleLet);
|
||||
}
|
||||
var res = new AstLetArray(name, fields, false);
|
||||
return res;
|
||||
}
|
||||
|
||||
private static AstStage ParseAddFields(JsonObject body)
|
||||
{
|
||||
var fields = new List<AstLet>();
|
||||
|
||||
var exclude = false;
|
||||
foreach (var field in body)
|
||||
{
|
||||
if ( field.Value is JsonArray ja )
|
||||
{
|
||||
var arrayProjection = ParseArrayProjection(field.Key, ja);
|
||||
fields.Add(arrayProjection);
|
||||
}
|
||||
else
|
||||
{
|
||||
var (let, exc) = ParseLet(field);
|
||||
exclude = exc;
|
||||
fields.Add(let);
|
||||
}
|
||||
}
|
||||
|
||||
var stage = new AstStageAddFields(fields);
|
||||
return stage;
|
||||
}
|
||||
|
||||
private static AstStage ParseBucket(JsonObject body)
|
||||
{
|
||||
var stage = new AstStageBucket();
|
||||
|
||||
foreach (var field in body)
|
||||
{
|
||||
switch (field)
|
||||
{
|
||||
case { Key: "groupBy", }:
|
||||
stage.GroupBy = ParseExpression(field.Value);
|
||||
break;
|
||||
case { Key: "default", Value: JsonValue val }:
|
||||
stage.DefaultBucket = val.ToString();
|
||||
break;
|
||||
case { Key: "boundaries", Value: JsonArray arr }:
|
||||
{
|
||||
foreach (var v in arr.Select(x => x?.ToString()).Where(x => x != null))
|
||||
stage.AddBucket(v!.Trim('"'));
|
||||
break;
|
||||
}
|
||||
case { Key: "output", Value: JsonObject output }:
|
||||
{
|
||||
foreach (var let in output)
|
||||
{
|
||||
if (let.Value is JsonArray ja)
|
||||
{
|
||||
var arrayProjection = ParseArrayProjection(let.Key, ja);
|
||||
stage.Add(arrayProjection);
|
||||
}
|
||||
else
|
||||
{
|
||||
var (let1, _) = ParseLet(let);
|
||||
stage.Add(let1);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stage;
|
||||
}
|
||||
|
||||
private static AstStage ParseBucketAuto(JsonObject body)
|
||||
{
|
||||
var stage = new AstStageBucket()
|
||||
{
|
||||
Auto = true
|
||||
};
|
||||
|
||||
foreach (var field in body)
|
||||
{
|
||||
switch (field)
|
||||
{
|
||||
case { Key: "groupBy", }:
|
||||
stage.GroupBy = ParseExpression(field.Value);
|
||||
break;
|
||||
case { Key: "buckets", Value: JsonValue val }:
|
||||
stage.NumberOfBuckets = val.GetValue<int>();
|
||||
break;
|
||||
case { Key: "granularity", Value: JsonValue val }:
|
||||
stage.Granularity = val.ToString();
|
||||
break;
|
||||
case { Key: "output", Value: JsonObject output }:
|
||||
{
|
||||
foreach (var let in output)
|
||||
{
|
||||
if (let.Value is JsonArray ja)
|
||||
{
|
||||
var arrayProjection = ParseArrayProjection(let.Key, ja);
|
||||
stage.Add(arrayProjection);
|
||||
}
|
||||
else
|
||||
{
|
||||
var (let1, _) = ParseLet(let);
|
||||
stage.Add(let1);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stage;
|
||||
}
|
||||
|
||||
private static AstStage ParseFacet(JsonObject body)
|
||||
{
|
||||
var stage = new AstStageFacet();
|
||||
|
||||
foreach (var (name, value) in body)
|
||||
{
|
||||
var pipeLine = ParsePipeline(value as JsonArray);
|
||||
|
||||
stage.Add( new AstNamedPipeline(name, new (pipeLine)));
|
||||
}
|
||||
|
||||
return stage;
|
||||
}
|
||||
|
||||
private static List<AstStage> ParsePipeline(JsonArray? json)
|
||||
{
|
||||
var pipeline = new List<AstStage>();
|
||||
if (json == null)
|
||||
return pipeline;
|
||||
|
||||
foreach ( var stageJson in json.OfType<JsonObject>())
|
||||
{
|
||||
var stage = ParseStage(stageJson);
|
||||
pipeline.Add(stage);
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
36
Rms.Risk.Mango.Language/Parsers/ErrorListener.cs
Normal file
36
Rms.Risk.Mango.Language/Parsers/ErrorListener.cs
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using Antlr4.Runtime;
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Parsers;
|
||||
|
||||
internal class ErrorListener<T>(string component) : IAntlrErrorListener<T>
|
||||
{
|
||||
private string Component { get; } = component;
|
||||
|
||||
public void SyntaxError( TextWriter output,
|
||||
IRecognizer recognizer,
|
||||
T offendingSymbol,
|
||||
int line,
|
||||
int charPositionInLine,
|
||||
string msg,
|
||||
RecognitionException e
|
||||
) =>
|
||||
throw new SyntaxErrorException(Component, line, charPositionInLine, msg, offendingSymbol!, e);
|
||||
}
|
||||
109
Rms.Risk.Mango.Language/Parsers/JsonListenerHelper.cs
Normal file
109
Rms.Risk.Mango.Language/Parsers/JsonListenerHelper.cs
Normal file
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Parsers;
|
||||
|
||||
public static class JsonListenerHelper
|
||||
{
|
||||
public static JsonNode Convert(MongoAggregationForHumansParser.JsonContext context)
|
||||
{
|
||||
if (context.@object() != null)
|
||||
{
|
||||
return ConvertToJsonObject(context.@object());
|
||||
}
|
||||
else if (context.array() != null)
|
||||
{
|
||||
return ConvertToJsonArray(context.array());
|
||||
}
|
||||
throw new($"Invalid json: {context?.GetType().Name}: {context}");
|
||||
}
|
||||
|
||||
private static JsonNode? ConvertToJsonValue(MongoAggregationForHumansParser.ValueContext context)
|
||||
{
|
||||
if (context.VARIABLE() != null)
|
||||
{
|
||||
return JsonValue.Create(context.VARIABLE().GetText().Trim('"'));
|
||||
}
|
||||
else if (context.STRING() != null)
|
||||
{
|
||||
return JsonValue.Create(context.STRING().GetText().Trim('"'));
|
||||
}
|
||||
else if (context.NUMBER() != null)
|
||||
{
|
||||
var d = double.Parse(context.NUMBER().GetText());
|
||||
if ( d == Math.Floor(d) )
|
||||
{
|
||||
if ( d >= int.MinValue || d <= int.MaxValue )
|
||||
return JsonValue.Create((int)d);
|
||||
return JsonValue.Create((long)d);
|
||||
}
|
||||
else
|
||||
return JsonValue.Create(d);
|
||||
}
|
||||
else if (context.GetText() == "true")
|
||||
{
|
||||
return JsonValue.Create(true);
|
||||
}
|
||||
else if (context.GetText() == "false")
|
||||
{
|
||||
return JsonValue.Create(false);
|
||||
}
|
||||
else if ( context.@object() != null )
|
||||
{
|
||||
return ConvertToJsonObject(context.@object());
|
||||
}
|
||||
else if ( context.array() != null )
|
||||
{
|
||||
return ConvertToJsonArray(context.array());
|
||||
}
|
||||
else if (context.GetText() == "null")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
throw new($"Invalid json value: {context?.GetType().Name}: {context}");
|
||||
}
|
||||
|
||||
private static JsonArray ConvertToJsonArray(MongoAggregationForHumansParser.ArrayContext context)
|
||||
{
|
||||
var array = new JsonArray();
|
||||
foreach (var value in context.value())
|
||||
{
|
||||
array.Add(ConvertToJsonValue(value));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private static JsonObject ConvertToJsonObject(MongoAggregationForHumansParser.ObjectContext context)
|
||||
{
|
||||
var pairs = new JsonObject();
|
||||
foreach (var pair in context.pair())
|
||||
{
|
||||
pairs.Add(ConvertToJsonPair(pair));
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
private static KeyValuePair<string, JsonNode?> ConvertToJsonPair(MongoAggregationForHumansParser.PairContext context)
|
||||
{
|
||||
var name = context.object_name().GetText().Trim('"');
|
||||
var value = ConvertToJsonValue(context.value());
|
||||
return new(name, value);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
346
Rms.Risk.Mango.Language/Parsers/ListenerHelper.cs
Normal file
346
Rms.Risk.Mango.Language/Parsers/ListenerHelper.cs
Normal file
@ -0,0 +1,346 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using Parser =Rms.Risk.Mango.Language.MongoAggregationForHumansParser;
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Parsers;
|
||||
|
||||
internal static class ListenerHelper
|
||||
{
|
||||
public static List<AstLet> BuildLet(Parser.Let_listContext? context)
|
||||
{
|
||||
var lets = new List<AstLet>();
|
||||
if ( context == null )
|
||||
return lets;
|
||||
|
||||
foreach (var let in context.let_list().SelectMany(BuildLet))
|
||||
{
|
||||
lets.Add(let);
|
||||
}
|
||||
if (context.let_list_item() != null)
|
||||
{
|
||||
var let = BuildSingleLet(context.let_list_item());
|
||||
lets.Add(let);
|
||||
}
|
||||
return lets;
|
||||
|
||||
static AstLet BuildSingleLet(Parser.Let_list_itemContext let)
|
||||
{
|
||||
switch (let)
|
||||
{
|
||||
//case Parser.LetVariableContext vc:
|
||||
// return new AstLetExpression(new AstExpressionVariable(vc.VARIABLE().GetText()));
|
||||
case Parser.LetExpressionAsContext ec:
|
||||
{
|
||||
var expression = BuildAst(ec.expression());
|
||||
|
||||
var name = ec.STRING()?.GetText()?.Trim('"');
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
name = ec.VARIABLE()?.GetText();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name) && expression is AstExpressionString se && se.Value.StartsWith("$"))
|
||||
expression = new AstExpressionVariable(se.Value[1..]);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name) && expression is not AstExpressionVariable)
|
||||
throw new($"Only plain variable allowed to be unnamed. Please add 'AS'. Got {ec.GetText()} ({expression.GetType()})");
|
||||
return new AstLetExpression(expression, name);
|
||||
}
|
||||
|
||||
case Parser.LetArrayContext ac:
|
||||
return ParseLetArray(ac);
|
||||
case Parser.LetObjectContext oc:
|
||||
return ParseLetObject(oc);
|
||||
default:
|
||||
throw new($"Invalid let: {let.GetType().Name}: {let.GetText()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AstLetArray ParseLetObject(Parser.LetObjectContext oc)
|
||||
{
|
||||
var fields = new List<AstLet>();
|
||||
foreach (var field in oc.let_list().let_list())
|
||||
{
|
||||
var x = BuildLet(field);
|
||||
fields.AddRange(x);
|
||||
}
|
||||
return new (oc.VARIABLE()?.GetText(), fields, false);
|
||||
}
|
||||
|
||||
private static AstLetArray ParseLetArray(Parser.LetArrayContext ac)
|
||||
{
|
||||
var fields = new List<AstLet>();
|
||||
foreach (var field in ac.let_list().let_list())
|
||||
{
|
||||
var x = BuildLet(field);
|
||||
fields.AddRange(x);
|
||||
}
|
||||
return new ((ac.VARIABLE() ?? ac.STRING())?.GetText(), fields, true);
|
||||
}
|
||||
|
||||
public static List<AstSortField> BuildSortFieldList(Parser.Sort_var_listContext context)
|
||||
{
|
||||
var lets = new List<AstSortField>();
|
||||
foreach (var let in context.sort_var_list().SelectMany(BuildSortFieldList))
|
||||
{
|
||||
lets.Add(let);
|
||||
}
|
||||
if (context.VARIABLE() != null)
|
||||
{
|
||||
var let = new AstSortField(context.VARIABLE().GetText(), AstSortField.SortOrder.Ascending);
|
||||
lets.Add(let);
|
||||
}
|
||||
return lets;
|
||||
}
|
||||
|
||||
public static List<AstLet> BuildVarList(Parser.Var_listContext context)
|
||||
{
|
||||
var lets = new List<AstLet>();
|
||||
foreach (var let in context.var_list().SelectMany(BuildVarList))
|
||||
{
|
||||
lets.Add(let);
|
||||
}
|
||||
if (context.VARIABLE() != null)
|
||||
{
|
||||
var let = new AstLetExpression(new AstExpressionVariable(context.VARIABLE().GetText()));
|
||||
lets.Add(let);
|
||||
}
|
||||
return lets;
|
||||
}
|
||||
|
||||
public static List<AstEquivalence> BuildEquivalence(Parser.Equivalence_listContext context)
|
||||
{
|
||||
var eqs = new List<AstEquivalence>();
|
||||
if (context is Parser.VarEquivalenceContext vc)
|
||||
{
|
||||
var left = new AstExpressionVariable(vc.left.Text);
|
||||
var right = new AstExpressionVariable(vc.right.Text);
|
||||
eqs.Add(new(left, right));
|
||||
}
|
||||
if (context is Parser.EquivalenceListContext ec)
|
||||
{
|
||||
foreach (var eq in ec.equivalence_list().SelectMany(BuildEquivalence))
|
||||
{
|
||||
eqs.Add(eq);
|
||||
}
|
||||
}
|
||||
return eqs;
|
||||
}
|
||||
|
||||
public static List<AstFunctionArgument> BuildUnnamedArgumentsList(Parser.Unnamed_args_listContext? context)
|
||||
{
|
||||
var arguments = new List<AstFunctionArgument>();
|
||||
foreach (var exp in context?.unnamed_args_list()?.SelectMany(BuildUnnamedArgumentsList) ?? [])
|
||||
{
|
||||
arguments.Add(exp);
|
||||
}
|
||||
if (context?.expression() != null)
|
||||
{
|
||||
var exp = BuildAst(context.expression());
|
||||
arguments.Add(new("", exp));
|
||||
}
|
||||
else if (context?.expression_array() != null)
|
||||
{
|
||||
var exp = BuildArray(context.expression_array());
|
||||
arguments.Add(new("", exp));
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public static AstExpressionArray BuildArray(Parser.Expression_arrayContext context)
|
||||
{
|
||||
var fields = new List<AstFunctionArgument>();
|
||||
foreach (var field in context.expression_array_item())
|
||||
{
|
||||
if ( field.expression() != null)
|
||||
{
|
||||
var exp = BuildAst(field.expression());
|
||||
fields.Add(new("", exp));
|
||||
}
|
||||
else if ( field.expression_array() != null)
|
||||
{
|
||||
var exp = BuildArray(field.expression_array());
|
||||
fields.Add(new("", exp));
|
||||
}
|
||||
}
|
||||
return new AstExpressionArray(fields);
|
||||
}
|
||||
|
||||
public static List<AstFunctionArgument> BuildNamedArgumentsList(Parser.Named_args_listContext? context)
|
||||
{
|
||||
var arguments = new List<AstFunctionArgument>();
|
||||
if (context == null)
|
||||
return arguments;
|
||||
|
||||
arguments.AddRange(context.named_args_list()?.SelectMany(BuildNamedArgumentsList) ?? []);
|
||||
if (context.expression() != null)
|
||||
{
|
||||
var exp = BuildAst(context.expression());
|
||||
arguments.Add(new(context.VARIABLE().GetText(), exp));
|
||||
}
|
||||
else if (context.expression_array() != null)
|
||||
{
|
||||
var exp = BuildArray(context.expression_array());
|
||||
arguments.Add(new(context.VARIABLE().GetText(), exp));
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public static AstExpression BuildAst(Parser.ExpressionContext context)
|
||||
{
|
||||
if ( context.comparizon_expression().Length == 1 )
|
||||
return BuildAst(context.comparizon_expression()[0]);
|
||||
|
||||
var op = context.AND().Length > 0
|
||||
? AstExpressionOperation.OperationType.AND
|
||||
: context.OR().Length > 0
|
||||
? AstExpressionOperation.OperationType.OR
|
||||
: throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}");
|
||||
|
||||
var list = new List<AstExpression>();
|
||||
foreach (var exp in context.comparizon_expression())
|
||||
{
|
||||
list.Add(BuildAst(exp));
|
||||
}
|
||||
return new AstExpressionOperation(op, list);
|
||||
}
|
||||
|
||||
public static AstExpression BuildAst(Parser.Comparizon_expressionContext context)
|
||||
{
|
||||
if ( context.additive_expression().Length == 1 )
|
||||
return BuildAst(context.additive_expression()[0]);
|
||||
|
||||
var op =
|
||||
context.LT().Length > 0
|
||||
? AstExpressionOperation.OperationType.LT
|
||||
: context.GT().Length > 0
|
||||
? AstExpressionOperation.OperationType.GT
|
||||
: context.EQ().Length > 0
|
||||
? AstExpressionOperation.OperationType.EQ
|
||||
: context.NEQ().Length > 0
|
||||
? AstExpressionOperation.OperationType.NEQ
|
||||
: context.LTE().Length > 0
|
||||
? AstExpressionOperation.OperationType.LTE
|
||||
: context.GTE().Length > 0
|
||||
? AstExpressionOperation.OperationType.GTE
|
||||
: throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}");
|
||||
|
||||
var list = new List<AstExpression>();
|
||||
foreach (var exp in context.additive_expression())
|
||||
{
|
||||
list.Add(BuildAst(exp));
|
||||
}
|
||||
return new AstExpressionOperation(op, list);
|
||||
}
|
||||
|
||||
public static AstExpression BuildAst(Parser.Additive_expressionContext context)
|
||||
{
|
||||
if ( context.multiplicative_expression().Length == 1 )
|
||||
return BuildAst(context.multiplicative_expression()[0]);
|
||||
|
||||
var op =
|
||||
context.PLUS().Length > 0
|
||||
? AstExpressionOperation.OperationType.PLUS
|
||||
: context.MINUS().Length > 0
|
||||
? AstExpressionOperation.OperationType.MINUS
|
||||
: throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}");
|
||||
|
||||
var list = new List<AstExpression>();
|
||||
foreach (var exp in context.multiplicative_expression())
|
||||
{
|
||||
list.Add(BuildAst(exp));
|
||||
}
|
||||
return new AstExpressionOperation(op, list);
|
||||
}
|
||||
|
||||
public static AstExpression BuildAst(Parser.Multiplicative_expressionContext context)
|
||||
{
|
||||
if ( context.unary_expression().Length == 1 )
|
||||
return BuildAst(context.unary_expression()[0]);
|
||||
|
||||
var op =
|
||||
context.MUL().Length > 0
|
||||
? AstExpressionOperation.OperationType.MULTIPLY
|
||||
: context.DIV().Length > 0
|
||||
? AstExpressionOperation.OperationType.DIVIDE
|
||||
: throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}");
|
||||
|
||||
var list = new List<AstExpression>();
|
||||
foreach (var exp in context.unary_expression())
|
||||
{
|
||||
list.Add(BuildAst(exp));
|
||||
}
|
||||
return new AstExpressionOperation(op, list);
|
||||
}
|
||||
|
||||
public static AstExpression BuildAst(Parser.Unary_expressionContext context)
|
||||
{
|
||||
var expr = context switch
|
||||
{
|
||||
Parser.UnaryExpressionContext unary =>
|
||||
new AstExpressionUnary(
|
||||
unary.NOT() != null
|
||||
? AstExpressionUnary.OperationType.NOT
|
||||
: unary.MINUS() != null
|
||||
? AstExpressionUnary.OperationType.MINUS
|
||||
: unary.PLUS() != null
|
||||
? AstExpressionUnary.OperationType.PLUS
|
||||
: throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}")
|
||||
, BuildAst(unary.unary_expression())),
|
||||
Parser.PrimaryExpressionContext brackets => BuildAst(brackets.brackets_expression()),
|
||||
_ => throw new($"Invalid expression: {context?.GetType().Name}: {context?.GetText()}")
|
||||
};
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
public static AstExpression BuildAst(Parser.Brackets_expressionContext context)
|
||||
{
|
||||
var expr = context switch
|
||||
{
|
||||
Parser.AtomExpressionContext atom => BuildAtom(atom.atom()),
|
||||
Parser.BracketsExpressionContext brackets => BuildAst(brackets.expression()),
|
||||
Parser.InExpressionContext @in => new AstExpressionIn(@in.VARIABLE().GetText(), @in.NOT() != null, @in.expression().Select(BuildAst)),
|
||||
Parser.FuncExpressionContext func => new AstExpressionFunctionCall(func.VARIABLE().GetText(), BuildUnnamedArgumentsList(func.unnamed_args_list()).Concat( BuildNamedArgumentsList(func.named_args_list())) ),
|
||||
Parser.ProjectionExpressionContext proj => new AstExpressionProjection(proj.VARIABLE().GetText(), JsonListenerHelper.Convert(proj.json())),
|
||||
Parser.ExistsExpressionContext exists => new AstExpressionExists(exists.VARIABLE().GetText(), exists.NOT() == null),
|
||||
|
||||
_ => throw new($"Invalid expression: {context?.GetType().Name}: {context}")
|
||||
};
|
||||
|
||||
return expr;
|
||||
|
||||
static AstExpression BuildAtom(Parser.AtomContext atom)
|
||||
{
|
||||
if (atom.STRING() != null)
|
||||
return new AstExpressionString(atom.STRING().GetText());
|
||||
else if (atom.NUMBER() != null)
|
||||
return new AstExpressionNumber(atom.NUMBER().GetText());
|
||||
else if (atom.GetText() == "true")
|
||||
return new AstExpressionBool(true);
|
||||
else if (atom.GetText() == "false")
|
||||
return new AstExpressionBool(false);
|
||||
else if (atom.GetText() == "null")
|
||||
return new AstExpressionNull();
|
||||
else if (atom.VARIABLE() != null)
|
||||
return new AstExpressionVariable(atom.VARIABLE().GetText());
|
||||
throw new($"Invalid atom: {atom?.GetType().Name}: {atom}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
363
Rms.Risk.Mango.Language/Parsers/MongoGrammarListener.cs
Normal file
363
Rms.Risk.Mango.Language/Parsers/MongoGrammarListener.cs
Normal file
@ -0,0 +1,363 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using Antlr4.Runtime.Misc;
|
||||
using Parser=Rms.Risk.Mango.Language.MongoAggregationForHumansParser;
|
||||
|
||||
namespace Rms.Risk.Mango.Language.Parsers;
|
||||
|
||||
public class MongoGrammarListener : MongoAggregationForHumansBaseListener
|
||||
{
|
||||
private AstPipeline _currentPipeline = AstPipeline.None;
|
||||
|
||||
private Stack<AstPipeline> _pipelines = new();
|
||||
private Stack<AstStage> _stage = new();
|
||||
|
||||
public AstAggregation? Aggregate { get; private set;}
|
||||
|
||||
private void Clear()
|
||||
{
|
||||
_currentPipeline = AstPipeline.None;
|
||||
_pipelines.Clear();
|
||||
_stage.Clear();
|
||||
}
|
||||
|
||||
public override void EnterFile(Parser.FileContext context)
|
||||
{
|
||||
Clear();
|
||||
var collection = context.STRING().GetText();
|
||||
Aggregate = new(collection);
|
||||
}
|
||||
|
||||
public override void ExitFile(Parser.FileContext context)
|
||||
{
|
||||
Aggregate!.Add(_currentPipeline);
|
||||
|
||||
if ( ReferenceEquals( _currentPipeline, AstPipeline.None ) )
|
||||
throw new("Invalid _currentPipeline");
|
||||
|
||||
if ( _pipelines.Count != 1 )
|
||||
throw new($"Invalid _pipelines Count={_pipelines.Count}");
|
||||
if ( _stage.Count != 0 )
|
||||
throw new($"Invalid _stage Count={_stage.Count}");
|
||||
}
|
||||
|
||||
public override void EnterPipeline_def(MongoAggregationForHumansParser.Pipeline_defContext context)
|
||||
{
|
||||
_pipelines.Push(_currentPipeline);
|
||||
_currentPipeline = new();
|
||||
|
||||
// stages are responsible for popping it back
|
||||
}
|
||||
|
||||
public override void ExitStage_def(Parser.Stage_defContext context)
|
||||
{
|
||||
var stage = _currentPipeline.Stages.Last();
|
||||
if ( context.json() != null )
|
||||
stage.Options = (JsonObject)JsonListenerHelper.Convert(context.json());
|
||||
}
|
||||
|
||||
public override void EnterMatch_def(Parser.Match_defContext context)
|
||||
{
|
||||
var stage = new AstStageWhere();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitMatch_def(Parser.Match_defContext context)
|
||||
{
|
||||
var stage = _stage.Pop();
|
||||
var expression = ListenerHelper.BuildAst(context.expression());
|
||||
stage.Add(expression);
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterBucketPlain(Parser.BucketPlainContext context)
|
||||
{
|
||||
var stage = new AstStageBucket();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitBucketPlain(Parser.BucketPlainContext context)
|
||||
{
|
||||
var stage = (AstStageBucket)_stage.Pop();
|
||||
|
||||
stage.GroupBy = ListenerHelper.BuildAst(context.expression());
|
||||
|
||||
foreach (var bound in context.NUMBER().Select(x => x.GetText()).Where(x => !string.IsNullOrWhiteSpace(x)))
|
||||
{
|
||||
stage.AddBucket(bound.Trim('"'));
|
||||
}
|
||||
|
||||
if (context.defaultBucket != null)
|
||||
{
|
||||
stage.DefaultBucket = context.defaultBucket.Text.Trim('"');
|
||||
}
|
||||
|
||||
var fields = ListenerHelper.BuildLet(context.let_list());
|
||||
foreach (var field in fields)
|
||||
stage.Add(field);
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterBucketAuto(Parser.BucketAutoContext context)
|
||||
{
|
||||
var stage = new AstStageBucket()
|
||||
{
|
||||
Auto = true
|
||||
};
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitBucketAuto(Parser.BucketAutoContext context)
|
||||
{
|
||||
var stage = (AstStageBucket)_stage.Pop();
|
||||
|
||||
stage.GroupBy = ListenerHelper.BuildAst(context.expression());
|
||||
stage.NumberOfBuckets = int.Parse(context.NUMBER().GetText());
|
||||
stage.Granularity = context.STRING()?.GetText().Trim('"');
|
||||
|
||||
var fields = ListenerHelper.BuildLet(context.let_list());
|
||||
foreach (var field in fields)
|
||||
stage.Add(field);
|
||||
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterAddfields_def(Parser.Addfields_defContext context)
|
||||
{
|
||||
var stage = new AstStageAddFields([]);
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitAddfields_def(Parser.Addfields_defContext context)
|
||||
{
|
||||
var stage = (AstStageAddFields)_stage.Pop();
|
||||
var fields = ListenerHelper.BuildLet(context.let_list());
|
||||
foreach (var field in fields)
|
||||
stage.Add(field);
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterProjectInclude(Parser.ProjectIncludeContext context)
|
||||
{
|
||||
var stage = new AstStageProject();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitProjectInclude(Parser.ProjectIncludeContext context)
|
||||
{
|
||||
var stage = (AstStageProject)_stage.Pop();
|
||||
|
||||
var idLetList = context.id_list;
|
||||
var letList = context.data_list;
|
||||
|
||||
var idFields = ListenerHelper.BuildLet(idLetList);
|
||||
foreach (var field in idFields)
|
||||
stage.AddId(field);
|
||||
|
||||
var fields = ListenerHelper.BuildLet(letList);
|
||||
foreach (var field in fields)
|
||||
stage.Add(field);
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterProjectExclude(Parser.ProjectExcludeContext context)
|
||||
{
|
||||
var stage = new AstStageProject();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitProjectExclude(Parser.ProjectExcludeContext context)
|
||||
{
|
||||
var stage = (AstStageProject)_stage.Pop();
|
||||
stage.Exclude = true;
|
||||
var fields = ListenerHelper.BuildVarList(context.var_list());
|
||||
foreach (var field in fields)
|
||||
stage.Add(field);
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterReplace_def(Parser.Replace_defContext context)
|
||||
{
|
||||
var stage = new AstStageReplace();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitReplace_def(Parser.Replace_defContext context)
|
||||
{
|
||||
var stage = (AstStageReplace)_stage.Pop();
|
||||
|
||||
var idLetList = context.id_list;
|
||||
var letList = context.data_list;
|
||||
|
||||
var idFields = ListenerHelper.BuildLet(idLetList);
|
||||
foreach (var field in idFields)
|
||||
stage.AddId(field);
|
||||
|
||||
var fields = ListenerHelper.BuildLet(letList);
|
||||
foreach (var field in fields)
|
||||
stage.Add(field);
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterGroup_by_def(Parser.Group_by_defContext context)
|
||||
{
|
||||
var stage = new AstStageGroupBy([], []);
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitGroup_by_def(Parser.Group_by_defContext context)
|
||||
{
|
||||
var stage = (AstStageGroupBy)_stage.Pop();
|
||||
var id = ListenerHelper.BuildLet(context.id_list);
|
||||
var fields = ListenerHelper.BuildLet(context.data_list);
|
||||
|
||||
foreach (var field in fields)
|
||||
stage.Add(field);
|
||||
foreach (var x in id)
|
||||
stage.AddId(x);
|
||||
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterSort_def(Parser.Sort_defContext context)
|
||||
{
|
||||
var stage = new AstStageSortBy();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitSort_def(Parser.Sort_defContext context)
|
||||
{
|
||||
var stage = (AstStageSortBy)_stage.Pop();
|
||||
foreach (var field in ListenerHelper.BuildSortFieldList(context.sort_var_list()))
|
||||
stage.Add(field);
|
||||
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterUnwind_def(Parser.Unwind_defContext context)
|
||||
{
|
||||
var stage = new AstStageUnwind();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitUnwind_def(Parser.Unwind_defContext context)
|
||||
{
|
||||
var stage = (AstStageUnwind)_stage.Pop();
|
||||
stage.Name = context.VARIABLE()[0].GetText();
|
||||
|
||||
stage.Index = context.VARIABLE().Length > 1 ? context.VARIABLE()[1].GetText() : null;
|
||||
|
||||
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterJoin_def(Parser.Join_defContext context)
|
||||
{
|
||||
var stage = new AstStageJoin();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitJoin_def(Parser.Join_defContext context)
|
||||
{
|
||||
var stage = (AstStageJoin)_stage.Pop();
|
||||
|
||||
var pipelineExists = context.pipeline_def() != null;
|
||||
|
||||
AstPipeline pipeline;
|
||||
if (pipelineExists)
|
||||
{
|
||||
pipeline = _currentPipeline;
|
||||
_currentPipeline = _pipelines.Pop();
|
||||
}
|
||||
else
|
||||
pipeline = AstPipeline.None;
|
||||
|
||||
var on = ListenerHelper.BuildEquivalence(context.equivalence_list());
|
||||
var collection = context.STRING()[0].GetText();
|
||||
|
||||
var asField =
|
||||
context.VARIABLE()?.GetText()
|
||||
?? context.STRING()[1].GetText()
|
||||
?? throw new ($"AS clause required for join: {context.GetText()}");
|
||||
|
||||
var let = ListenerHelper.BuildLet(context.let_list());
|
||||
|
||||
stage.Init(collection, asField, on, let, !pipelineExists || pipeline.Count == 0 ? null : pipeline);
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterFacet_def(Parser.Facet_defContext context)
|
||||
{
|
||||
var stage = new AstStageFacet();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitFacet_def(Parser.Facet_defContext context)
|
||||
{
|
||||
var stage = (AstStageFacet)_stage.Pop();
|
||||
var pipelines = new List<AstPipeline>{_currentPipeline};
|
||||
|
||||
if (context.pipeline_def().Length != context.VARIABLE().Length)
|
||||
throw new($"context.pipeline_def().Length={context.pipeline_def().Length} != context.VARIABLE().Length={context.VARIABLE().Length}");
|
||||
|
||||
if (_pipelines.Count <= context.pipeline_def().Length)
|
||||
throw new($"_pipelines.Count={_pipelines.Count} but context.pipeline_def().Length={context.pipeline_def().Length}");
|
||||
|
||||
for (var i = 0; i < context.pipeline_def().Length-1; i++)
|
||||
{
|
||||
pipelines.Insert(0, _pipelines.Pop());
|
||||
}
|
||||
|
||||
for (var i = 0; i < pipelines.Count; i++)
|
||||
{
|
||||
var name = context.VARIABLE(i).GetText();
|
||||
var pipeline = pipelines[i];
|
||||
var namedPipeline = new AstNamedPipeline(name, pipeline);
|
||||
|
||||
stage.Add( namedPipeline );
|
||||
}
|
||||
|
||||
if (_pipelines.Count < 1)
|
||||
throw new($"Invalid _pipelines Count={_pipelines.Count}");
|
||||
|
||||
_currentPipeline = _pipelines.Pop();
|
||||
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
public override void EnterDo_def([NotNull] Parser.Do_defContext context)
|
||||
{
|
||||
var stage = new AstStageDo();
|
||||
_stage.Push(stage);
|
||||
}
|
||||
|
||||
public override void ExitDo_def([NotNull] Parser.Do_defContext context)
|
||||
{
|
||||
var stage = (AstStageDo)_stage.Pop();
|
||||
|
||||
var json = JsonListenerHelper.Convert(context.json());
|
||||
stage.Json = json;
|
||||
|
||||
_currentPipeline.Add(stage);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
76
Rms.Risk.Mango.Language/Parsers/SyntaxErrorException.cs
Normal file
76
Rms.Risk.Mango.Language/Parsers/SyntaxErrorException.cs
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Language.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Script parsing syntax error
|
||||
/// </summary>
|
||||
public class SyntaxErrorException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Line number
|
||||
/// </summary>
|
||||
public int Line { get; }
|
||||
/// <summary>
|
||||
/// Position within the line
|
||||
/// </summary>
|
||||
public int Position { get; }
|
||||
/// <summary>
|
||||
/// Offending symbol
|
||||
/// </summary>
|
||||
public object? Symbol { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Source of error
|
||||
/// </summary>
|
||||
public string Component { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="component"></param>
|
||||
/// <param name="line"></param>
|
||||
/// <param name="pos"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="symbol"></param>
|
||||
/// <param name="innerException"></param>
|
||||
public SyntaxErrorException( string component, int line, int pos, string message, object? symbol = null, Exception? innerException = null )
|
||||
: base($"Syntax error in {component}: line {line} position {pos}: {message}", innerException)
|
||||
{
|
||||
Line = line;
|
||||
Position = pos;
|
||||
Symbol = symbol;
|
||||
Component = component;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="component"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="innerException"></param>
|
||||
public SyntaxErrorException( string component, string message, Exception? innerException = null )
|
||||
: base($"Syntax error in {component}: {message}", innerException)
|
||||
{
|
||||
Line = 0;
|
||||
Position = 0;
|
||||
Symbol = null;
|
||||
Component = component;
|
||||
}
|
||||
}
|
||||
39
Rms.Risk.Mango.Language/README.md
Normal file
39
Rms.Risk.Mango.Language/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# AFH language
|
||||
|
||||
See [documentation](../Rms.Risk.Mango/wwwroot/docs/afh-overview.md).
|
||||
|
||||
# Compiling
|
||||
|
||||
Unfortunately there is something is wrong with `Antlr4` tooling. According to https://github.com/kaby76/Antlr4BuildTasks/blob/master/Readme.md the
|
||||
project is setup correctly however we are experiencing the following error:
|
||||
|
||||
> ANT02: Went through the complete probe list looking for an Antlr4 tool jar, but could not find anything
|
||||
|
||||
To workaround it you can use one of the options:
|
||||
|
||||
1. To download it automatically during the build, uncomment this section within Rms.Risk.Mango.Language.csproj :
|
||||
```
|
||||
<!--
|
||||
<PropertyGroup>
|
||||
<AntlrJarDir>$(SolutionDirectory)Resources/Antlr4</AntlrJarDir>
|
||||
<AntlrJarFile>antlr4-4.13.1-complete.jar</AntlrJarFile>
|
||||
<AntlrJarPath>$(AntlrJarDir)/$(AntlrJarFile)</AntlrJarPath>
|
||||
<AntlrDownloadUrl>https://www.antlr.org/download/antlr-4.13.2-complete.jar</AntlrDownloadUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="EnsureAntlr4Jar" BeforeTargets="Antlr4Compile">
|
||||
<Message Text="Ensuring ANTLR jar exists at $(AntlrJarPath)" Importance="High" />
|
||||
<MakeDir Directories="$(AntlrJarDir)" Condition="!Exists('$(AntlrJarDir)')" />
|
||||
<Exec Condition="!Exists('$(AntlrJarPath)') AND '$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'"
|
||||
Command="powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri $(AntlrDownloadUrl) -OutFile '$(AntlrJarPath)'"" />
|
||||
<Exec Condition="!Exists('$(AntlrJarPath)') AND '$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' != 'true'"
|
||||
Command="bash -c "curl -L -o '$(AntlrJarPath)' $(AntlrDownloadUrl)"" />
|
||||
<Message Condition="Exists('$(AntlrJarPath)')" Text="ANTLR jar present: $(AntlrJarPath)" Importance="High" />
|
||||
</Target>
|
||||
-->
|
||||
```
|
||||
|
||||
2. Download `jar` https://www.antlr.org/download.html (direct link https://www.antlr.org/download/antlr-4.13.2-complete.jar)
|
||||
and put it into `$(SolutionDirectory)Resources/Antlr4` folder. Create one if needed.
|
||||
|
||||
Now it should work.
|
||||
45
Rms.Risk.Mango.Language/Rms.Risk.Mango.Language.csproj
Normal file
45
Rms.Risk.Mango.Language/Rms.Risk.Mango.Language.csproj
Normal file
@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<NoWarn>CS1584,CS1658,CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
<PropertyGroup>
|
||||
<AntlrJarDir>$(SolutionDirectory)tools/Antlr4</AntlrJarDir>
|
||||
<AntlrJarFile>antlr4-4.13.1-complete.jar</AntlrJarFile>
|
||||
<AntlrJarPath>$(AntlrJarDir)/$(AntlrJarFile)</AntlrJarPath>
|
||||
<AntlrDownloadUrl>https://www.antlr.org/download/antlr-4.13.2-complete.jar</AntlrDownloadUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="EnsureAntlr4Jar" BeforeTargets="Antlr4Compile">
|
||||
<Message Text="Ensuring ANTLR jar exists at $(AntlrJarPath)" Importance="High" />
|
||||
<MakeDir Directories="$(AntlrJarDir)" Condition="!Exists('$(AntlrJarDir)')" />
|
||||
<Exec Condition="!Exists('$(AntlrJarPath)') AND '$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'"
|
||||
Command="powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri $(AntlrDownloadUrl) -OutFile '$(AntlrJarPath)'"" />
|
||||
<Exec Condition="!Exists('$(AntlrJarPath)') AND '$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' != 'true'"
|
||||
Command="bash -c "curl -L -o '$(AntlrJarPath)' $(AntlrDownloadUrl)"" />
|
||||
<Message Condition="Exists('$(AntlrJarPath)')" Text="ANTLR jar present: $(AntlrJarPath)" Importance="High" />
|
||||
</Target>
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<Antlr4 Include="MongoAggregationForHumans.g4">
|
||||
<Package>Rms.Risk.Mango.Language</Package>
|
||||
<AntlrToolJar>$(SolutionDirectory)tools/Antlr4/antlr4-4.13.1-complete.jar</AntlrToolJar>
|
||||
<JavaExec Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">$(JAVA_HOME)/bin/java.exe</JavaExec>
|
||||
<JavaExec Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' != 'true'">$(JAVA_HOME)/bin/java</JavaExec>
|
||||
</Antlr4>
|
||||
<Antlr4 Include="JsonGrammar.g4">
|
||||
<Package>Rms.Risk.Mango.Language</Package>
|
||||
<AntlrToolJar>$(SolutionDirectory)tools/Antlr4/antlr4-4.13.1-complete.jar</AntlrToolJar>
|
||||
<JavaExec Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">$(JAVA_HOME)/bin/java.exe</JavaExec>
|
||||
<JavaExec Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' != 'true'">$(JAVA_HOME)/bin/java</JavaExec>
|
||||
</Antlr4>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Antlr4.Runtime.Standard" />
|
||||
<PackageReference Include="Antlr4BuildTasks" GeneratePathProperty="true" PrivateAssets="all" IncludeAssets="build" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
23
Rms.Risk.Mango.Language/_imports.cs
Normal file
23
Rms.Risk.Mango.Language/_imports.cs
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
global using System.Text.Json.Nodes;
|
||||
global using Rms.Risk.Mango.Language.Ast;
|
||||
global using Rms.Risk.Mango.Language.Parsers;
|
||||
331
Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotData.cs
Normal file
331
Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotData.cs
Normal file
@ -0,0 +1,331 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Diagnostics;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core;
|
||||
|
||||
[BsonSerializer( typeof(ArrayBasedPivotDataSerializer))]
|
||||
[JsonConverter( typeof(ArrayBasedPivotDataJsonSerializer))]
|
||||
public class ArrayBasedPivotData : IPivotedData
|
||||
{
|
||||
private readonly List<string> _headers;
|
||||
private List<(string OrigHeader, string DisplayHeader)>? _headersMap;
|
||||
private readonly List<object?[]> _realData = [];
|
||||
private readonly Dictionary<int, Type> _columnTypesCache = [];
|
||||
private readonly List<string> _displayHeaders = [];
|
||||
private int[]? _columnMap; // displayPos -> physicalPos in _headers
|
||||
|
||||
|
||||
public static readonly ArrayBasedPivotData NoData = new(["No data"]) { Id = "no_data" };
|
||||
|
||||
|
||||
public IReadOnlyCollection<string> Headers
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _displayHeaders.Count > 0 && _displayHeaders.Count == _headers.Count)
|
||||
return _displayHeaders;
|
||||
|
||||
if ( !(_columnMap?.Length > 0) )
|
||||
return _headers;
|
||||
|
||||
_displayHeaders.Clear();
|
||||
|
||||
_displayHeaders.AddRange(
|
||||
_columnMap
|
||||
.Select((physicalPos, displayPos) => (physicalPos, displayPos))
|
||||
.OrderBy(x => x.displayPos)
|
||||
.Select( x => _headers[x.physicalPos])
|
||||
);
|
||||
|
||||
// _displayHeaders.AddRange(
|
||||
// _headers
|
||||
// .Select((x, i) => (Name: x, Order: Array.IndexOf(_columnMap, i, 0)))
|
||||
// .OrderBy(x => x.Order)
|
||||
// .Select(x => x.Name)
|
||||
// .ToList()
|
||||
// );
|
||||
|
||||
return _displayHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<(string OrigHeader, string DesplayHeader)> HeadersMap => _headersMap ?? [];
|
||||
|
||||
public void UpdateHeaders(Func<string, string> changeColumnName)
|
||||
{
|
||||
// make a copy or original headers
|
||||
if ( _headersMap != null )
|
||||
throw new ApplicationException("UpdateHeaders can only be called once");
|
||||
_headersMap = [];
|
||||
|
||||
for (var i = 0; i < _headers.Count; i++ )
|
||||
{
|
||||
var newName = changeColumnName(_headers[i]);
|
||||
_headersMap.Add((OrigHeader:_headers[i], DisplayHeader: newName));
|
||||
_headers[i] = newName;
|
||||
}
|
||||
|
||||
_displayHeaders.Clear();
|
||||
}
|
||||
|
||||
public string Id { get; set; } = "";
|
||||
public DateTime ExpireAt { get; set; }
|
||||
|
||||
public int Count => _realData.Count;
|
||||
|
||||
public object? Get(int displayCol, int row) => this[displayCol, row];
|
||||
|
||||
public object? this[int displayCol, int row]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (row >= _realData.Count)
|
||||
throw new ArgumentException($"Row={row} is greater than Count={_realData.Count}");
|
||||
|
||||
var physicalCol = _columnMap != null && displayCol < _columnMap.Length
|
||||
? _columnMap[displayCol]
|
||||
: displayCol;
|
||||
|
||||
return physicalCol >= _realData[row].Length
|
||||
? null
|
||||
: _realData[row][physicalCol];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (row >= _realData.Count)
|
||||
throw new ArgumentException($"Row={row} is greater than Count={_realData.Count}");
|
||||
|
||||
var physicalCol = _columnMap != null && displayCol < _columnMap.Length
|
||||
? _columnMap[displayCol]
|
||||
: displayCol;
|
||||
|
||||
if (physicalCol >= _realData[row].Length)
|
||||
throw new ArgumentException($"Col={row} is greater than Count={_realData[row].Length} or Row={row}");
|
||||
_realData[row][physicalCol] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayBasedPivotData(IEnumerable<string> headers)
|
||||
{
|
||||
_headers = headers.ToList();
|
||||
}
|
||||
|
||||
public ArrayBasedPivotData(IPivotedData other)
|
||||
: this( other.Headers )
|
||||
{
|
||||
var len = other.Headers.Count;
|
||||
var o = new object?[len];
|
||||
|
||||
for ( var row = 0; row < other.Count; row++)
|
||||
{
|
||||
for (var col = 0; col < len; col++)
|
||||
{
|
||||
o[col] = other.Get(col, row);
|
||||
}
|
||||
Add(o);
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayBasedPivotData(
|
||||
IEnumerable<string> headers,
|
||||
int[]? columnMap
|
||||
)
|
||||
{
|
||||
_headers = headers.ToList();
|
||||
_columnMap = columnMap == null ? null : [.. columnMap]; // make a copy
|
||||
}
|
||||
|
||||
public void Add( IEnumerable<object?> data )
|
||||
{
|
||||
var row = new object?[_headers.Count];
|
||||
var i = 0;
|
||||
foreach ( var o in data )
|
||||
{
|
||||
if ( i >= _headers.Count )
|
||||
throw new ArgumentException($"Length of supplied data must be at least {_headers.Count}", nameof(data));
|
||||
row[i++] = o;
|
||||
}
|
||||
|
||||
_realData.Add( row );
|
||||
}
|
||||
|
||||
public void AddHeader(string header)
|
||||
{
|
||||
_headers.Add(header);
|
||||
_displayHeaders.Clear();
|
||||
_columnMap = null;
|
||||
}
|
||||
|
||||
public bool Contains( string header ) => _headers.Any( x => x == header );
|
||||
|
||||
public Type GetColumnType(int displayCol)
|
||||
{
|
||||
if ( _columnTypesCache.TryGetValue( displayCol, out var t ) )
|
||||
return t;
|
||||
t = DetectColumnType( displayCol );
|
||||
_columnTypesCache[displayCol] = t;
|
||||
return t;
|
||||
}
|
||||
|
||||
private Type DetectColumnType(int displayCol)
|
||||
{
|
||||
var guessDouble = 0;
|
||||
var guessLong = 0;
|
||||
var guessInt = 0;
|
||||
var guessDec = 0;
|
||||
|
||||
for ( var i = 0; i < Math.Min( 200, Count); i++ )
|
||||
{
|
||||
var o = this[displayCol, i];
|
||||
if (o == null)
|
||||
continue;
|
||||
|
||||
if ( o is double )
|
||||
guessDouble += 1;
|
||||
else if (o is int)
|
||||
guessInt += 1;
|
||||
else if (o is long)
|
||||
guessLong += 1;
|
||||
else if (o is decimal)
|
||||
guessDec += 1;
|
||||
}
|
||||
|
||||
if (guessDec > guessDouble && guessDec > guessLong && guessDec > guessInt)
|
||||
return typeof(decimal);
|
||||
if (guessDouble > guessLong && guessDouble > guessInt)
|
||||
return typeof(double);
|
||||
if (guessLong > guessInt)
|
||||
return typeof(long);
|
||||
if (guessInt > 0)
|
||||
return typeof(int);
|
||||
|
||||
return typeof(string);
|
||||
}
|
||||
|
||||
public void ReorderColumns( IReadOnlyCollection<string> columnsOrder )
|
||||
{
|
||||
_displayHeaders.Clear();
|
||||
|
||||
if ( !(columnsOrder?.Count > 0) )
|
||||
{
|
||||
_columnMap = [];
|
||||
return;
|
||||
}
|
||||
|
||||
var src = new List<string>( _headers );
|
||||
var colMap = new List<int>(); // position = display col / value = real col number
|
||||
|
||||
foreach ( var regex in columnsOrder.Select( x => new Regex( x, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline ) ).ToArray() )
|
||||
{
|
||||
var alpha = new List<string>(); // for alpha sorting
|
||||
foreach ( var name in src )
|
||||
{
|
||||
var m = regex.Match( name );
|
||||
if ( !m.Success )
|
||||
continue;
|
||||
alpha.Add( name );
|
||||
}
|
||||
|
||||
alpha.Sort();
|
||||
|
||||
foreach ( var name in alpha )
|
||||
{
|
||||
src.Remove( name );
|
||||
colMap.Add( _headers.IndexOf( name ) );
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var name in src) // something what is not fall under any regex
|
||||
colMap.Add(_headers.IndexOf(name));
|
||||
|
||||
Debug.Assert( colMap.Count == _headers.Count );
|
||||
|
||||
_columnMap = colMap.ToArray(); // set column mapping
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create filtered copy of data. Filter func must return true if you want row to remain in the output copy.
|
||||
/// </summary>
|
||||
/// <param name="filter"></param>
|
||||
/// <returns></returns>
|
||||
public IPivotedData Filter(Func<int, bool> filter)
|
||||
{
|
||||
var res = new ArrayBasedPivotData(_headers, _columnMap );
|
||||
for( var i = 0; i < _realData.Count; i++ )
|
||||
{
|
||||
if (!filter(i))
|
||||
continue;
|
||||
res.Add(_realData[i]);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create filtered copy of data. Resulting columns present in the data in order of appearance in newColumns. If column is missing it's omitted.
|
||||
/// </summary>
|
||||
public IPivotedData FilterColumns(IReadOnlyCollection<string> newColumns, Func<int, bool>? filter = null)
|
||||
{
|
||||
var newColMap = new List<(string, int)>();
|
||||
foreach (var colName in newColumns)
|
||||
{
|
||||
var index = _headers.IndexOf(colName);
|
||||
if ( index < 0 )
|
||||
continue;
|
||||
newColMap.Add((colName, index));
|
||||
}
|
||||
|
||||
var res = new ArrayBasedPivotData( newColMap.Select(x => x.Item1).ToList() );
|
||||
for( var i = 0; i < _realData.Count; i++ )
|
||||
{
|
||||
if (filter != null && !filter(i))
|
||||
continue;
|
||||
|
||||
var newData = new object?[newColMap.Count];
|
||||
foreach (var (oldIndex, newIndex) in newColMap.Select((x,pos) => (OldIndex: x.Item2, NewIndex: pos)))
|
||||
{
|
||||
newData[newIndex] = _realData[i].Length <= oldIndex
|
||||
? null
|
||||
: _realData[i][oldIndex]
|
||||
;
|
||||
}
|
||||
res.Add(newData);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
private class RowComparer(IPivotedData pivot, Func<object?[], object?[], Dictionary<string, int>, int> comparer) : IComparer<object?[]>
|
||||
{
|
||||
private readonly Dictionary<string, int> _columnMap = pivot.Headers.Select((x, i) => (x, i)).ToDictionary(x => x.x, x => x.i);
|
||||
private readonly Func<object?[], object?[], Dictionary<string, int>, int> _comparer = comparer;
|
||||
|
||||
public int Compare( object?[]? x, object?[]? y )
|
||||
=> x == null ? -1 : y == null ? 1 : _comparer( x, y, _columnMap );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom sort for real data. Be careful with views.
|
||||
/// </summary>
|
||||
/// <param name="comparer"></param>
|
||||
public void Sort( Func<object?[], object?[], Dictionary<string, int>, int> comparer ) => _realData.Sort(new RowComparer( this, comparer ));
|
||||
}
|
||||
400
Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataJsonSerializer.cs
Normal file
400
Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataJsonSerializer.cs
Normal file
@ -0,0 +1,400 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Runtime.Serialization;
|
||||
#if MSJSON
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
#else
|
||||
using Newtonsoft.Json;
|
||||
#endif
|
||||
namespace Rms.Risk.Mango.Pivot.Core;
|
||||
#if MSJSON
|
||||
public class ArrayBasedPivotDataJsonSerializer : JsonConverter<ArrayBasedPivotData>
|
||||
{
|
||||
public override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
ArrayBasedPivotData value,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
if ( string.IsNullOrWhiteSpace( value.Id ) )
|
||||
throw new SerializationException( "Id is not set for ArrayBasedPivotData instance" );
|
||||
var headers = value.Headers.OrderBy(x => x.Value).Select(x => x.Key).ToList();
|
||||
if ( headers.Count == 0 )
|
||||
throw new SerializationException("Headers count is zero for ArrayBasedPivotData instance");
|
||||
|
||||
writer.WriteStartObject();
|
||||
|
||||
writer.WritePropertyName("_id");
|
||||
writer.WriteStringValue(value.Id);
|
||||
writer.WritePropertyName("ExpireAt");
|
||||
writer.WriteStringValue(value.ExpireAt.ToString("O"));
|
||||
|
||||
writer.WritePropertyName("Headers");
|
||||
|
||||
writer.WriteStartArray();
|
||||
foreach ( var header in headers )
|
||||
writer.WriteStringValue( header );
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName( "Data" );
|
||||
writer.WriteStartArray();
|
||||
|
||||
for ( var row = 0; row < value.Count; row ++ )
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
|
||||
for ( var col = 0; col < headers.Count; col++ )
|
||||
{
|
||||
var data = value.Get( col, row );
|
||||
if ( data == null )
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
var t = data.GetType();
|
||||
if (t == typeof(double))
|
||||
writer.WriteNumberValue((double)data);
|
||||
else if (t == typeof(string))
|
||||
writer.WriteStringValue((string)data);
|
||||
else if (t == typeof(int))
|
||||
writer.WriteNumberValue((int)data);
|
||||
else if (t == typeof(long))
|
||||
writer.WriteNumberValue((long)data);
|
||||
else if (t == typeof(DateTime))
|
||||
writer.WriteStringValue(((DateTime)data).ToString("O"));
|
||||
else if (t == typeof(bool))
|
||||
writer.WriteBooleanValue((bool)data);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static void Expect( Utf8JsonReader reader, JsonTokenType expected )
|
||||
{
|
||||
if ( !reader.Read() )
|
||||
throw new SerializationException();
|
||||
if ( reader.TokenType != expected )
|
||||
throw new SerializationException($"Expected {expected} but got {reader.TokenType}");
|
||||
}
|
||||
|
||||
private static void ReadStartArray( Utf8JsonReader reader ) => Expect( reader, JsonTokenType.StartArray );
|
||||
private static void ReadEndProperty( Utf8JsonReader reader ) => Expect( reader, JsonTokenType.EndObject );
|
||||
|
||||
private static void ReadProperty( Utf8JsonReader reader, string name )
|
||||
{
|
||||
Expect( reader, JsonTokenType.PropertyName );
|
||||
var n = reader.GetString();
|
||||
if ( n != name )
|
||||
throw new SerializationException($"Expected \"{name}\" but got \"{n}\"");
|
||||
}
|
||||
|
||||
|
||||
public override ArrayBasedPivotData Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
ReadProperty(reader, "_id");
|
||||
var id = reader.GetString();
|
||||
ReadProperty(reader, "ExpireAt");
|
||||
var expireAt = DateTime.Parse(reader.GetString());
|
||||
|
||||
ReadProperty(reader, "Headers");
|
||||
|
||||
ReadStartArray(reader);
|
||||
var headers = new List<string>();
|
||||
|
||||
while ( reader.Read() )
|
||||
{
|
||||
var tokenType = reader.TokenType;
|
||||
if ( tokenType == JsonTokenType.EndArray )
|
||||
break;
|
||||
headers.Add( reader.GetString() );
|
||||
}
|
||||
|
||||
var data = new ArrayBasedPivotData( headers )
|
||||
{
|
||||
Id = id,
|
||||
ExpireAt = expireAt
|
||||
};
|
||||
|
||||
ReadProperty(reader, "Data");
|
||||
|
||||
while ( reader.Read() )
|
||||
{
|
||||
var tokenType = reader.TokenType;
|
||||
if ( tokenType == JsonTokenType.EndArray )
|
||||
break;
|
||||
|
||||
if ( tokenType != JsonTokenType.StartArray )
|
||||
continue;
|
||||
|
||||
var val = new object[headers.Count];
|
||||
var col = 0;
|
||||
|
||||
while ( reader.Read() )
|
||||
{
|
||||
tokenType = reader.TokenType;
|
||||
if ( tokenType == JsonTokenType.StartArray )
|
||||
continue;
|
||||
if ( tokenType == JsonTokenType.EndArray )
|
||||
break;
|
||||
|
||||
switch ( tokenType )
|
||||
{
|
||||
case JsonTokenType.Comment:
|
||||
case JsonTokenType.None:
|
||||
break;
|
||||
// case JsonTokenType.Float:
|
||||
// case JsonTokenType.Boolean:
|
||||
// case JsonTokenType.Date:
|
||||
// case JsonTokenType.Integer:
|
||||
// case JsonTokenType.Bytes:
|
||||
case JsonTokenType.String:
|
||||
val[col] = reader.GetString();
|
||||
break;
|
||||
case JsonTokenType.Number:
|
||||
var s = reader.GetString();
|
||||
// since doubles are more than 90% of the data make it quick
|
||||
if ( s.IndexOf(".", StringComparison.Ordinal) >= 0 && double.TryParse( s, out var d ) )
|
||||
val[col] = d;
|
||||
if ( int.TryParse( s, out var i ) )
|
||||
val[col] = i;
|
||||
if ( long.TryParse( s, out var l ) )
|
||||
val[col] = l;
|
||||
if ( double.TryParse( s, out d ) )
|
||||
val[col] = d;
|
||||
break;
|
||||
case JsonTokenType.Null:
|
||||
val[col] = null;
|
||||
break;
|
||||
// case JsonToken.StartObject:
|
||||
// case JsonToken.StartArray:
|
||||
// case JsonToken.StartConstructor:
|
||||
// case JsonToken.PropertyName:
|
||||
// case JsonToken.Raw:
|
||||
// case JsonToken.Undefined:
|
||||
// case JsonToken.EndObject:
|
||||
// case JsonToken.EndConstructor:
|
||||
default:
|
||||
throw new SerializationException($"Unexpected token {tokenType}");
|
||||
}
|
||||
|
||||
col += 1;
|
||||
}
|
||||
|
||||
data.Add( val );
|
||||
}
|
||||
|
||||
ReadEndProperty( reader );
|
||||
return data;
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(ArrayBasedPivotData) == objectType;
|
||||
}
|
||||
}
|
||||
#else
|
||||
public class ArrayBasedPivotDataJsonSerializer : JsonConverter
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, object? val, JsonSerializer serializer)
|
||||
{
|
||||
if ( val is not ArrayBasedPivotData value )
|
||||
throw new ArgumentNullException( nameof(value), "ArrayBasedPivotData instance is null" );
|
||||
if ( string.IsNullOrWhiteSpace( value.Id ) )
|
||||
throw new SerializationException( "Id is not set for ArrayBasedPivotData instance" );
|
||||
var headers = value.Headers;
|
||||
if ( headers.Count == 0 )
|
||||
throw new SerializationException("Headers count is zero for ArrayBasedPivotData instance");
|
||||
|
||||
writer.WriteStartObject();
|
||||
|
||||
writer.WritePropertyName("_id");
|
||||
writer.WriteValue(value.Id);
|
||||
writer.WritePropertyName("ExpireAt");
|
||||
writer.WriteValue(value.ExpireAt);
|
||||
|
||||
writer.WritePropertyName("Headers");
|
||||
|
||||
writer.WriteStartArray();
|
||||
foreach ( var header in headers )
|
||||
writer.WriteValue( header );
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName( "Data" );
|
||||
writer.WriteStartArray();
|
||||
|
||||
for ( var row = 0; row < value.Count; row ++ )
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
|
||||
for ( var col = 0; col < headers.Count; col++ )
|
||||
{
|
||||
var data = value.Get( col, row );
|
||||
if ( data == null )
|
||||
{
|
||||
writer.WriteNull();
|
||||
continue;
|
||||
}
|
||||
|
||||
var t = data.GetType();
|
||||
if (t == typeof(double))
|
||||
writer.WriteValue((double)data);
|
||||
else if (t == typeof(string))
|
||||
writer.WriteValue((string)data);
|
||||
else if (t == typeof(int))
|
||||
writer.WriteValue((int)data);
|
||||
else if (t == typeof(long))
|
||||
writer.WriteValue((long)data);
|
||||
else if (t == typeof(DateTime))
|
||||
writer.WriteValue((DateTime)data);
|
||||
else if (t == typeof(bool))
|
||||
writer.WriteValue((bool)data);
|
||||
else
|
||||
writer.WriteNull();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static void Expect( JsonReader reader, JsonToken expected )
|
||||
{
|
||||
if ( !reader.Read() )
|
||||
throw new SerializationException($"Expected {expected} but got none");
|
||||
if ( reader.TokenType != expected )
|
||||
throw new SerializationException($"Expected {expected} but got {reader.TokenType}");
|
||||
}
|
||||
|
||||
private static void ReadStartArray( JsonReader reader ) => Expect( reader, JsonToken.StartArray );
|
||||
private static void ReadEndProperty( JsonReader reader ) => Expect( reader, JsonToken.EndObject );
|
||||
|
||||
private static void ReadProperty( JsonReader reader, string name )
|
||||
{
|
||||
Expect( reader, JsonToken.PropertyName );
|
||||
if ( reader.ValueType != typeof(string))
|
||||
throw new SerializationException($"Expected {name} but got \"{reader.Value}\" ({reader.ValueType})");
|
||||
var n = reader.Value as string;
|
||||
if ( n != name )
|
||||
throw new SerializationException($"Expected \"{name}\" but got \"{n}\"");
|
||||
}
|
||||
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
ReadProperty(reader, "_id");
|
||||
var id = reader.ReadAsString();
|
||||
ReadProperty(reader, "ExpireAt");
|
||||
var expireAt = reader.ReadAsDateTime();
|
||||
|
||||
ReadProperty(reader, "Headers");
|
||||
|
||||
ReadStartArray(reader);
|
||||
var headers = new List<string>();
|
||||
|
||||
while ( reader.Read() )
|
||||
{
|
||||
var tokenType = reader.TokenType;
|
||||
if ( tokenType == JsonToken.EndArray )
|
||||
break;
|
||||
headers.Add( (string)reader.Value! );
|
||||
}
|
||||
|
||||
var data = new ArrayBasedPivotData( headers )
|
||||
{
|
||||
Id = id ?? "",
|
||||
ExpireAt = expireAt ?? default(DateTime)
|
||||
};
|
||||
|
||||
if (data.Id == "no_data")
|
||||
return data;
|
||||
|
||||
ReadProperty(reader, "Data");
|
||||
|
||||
while ( reader.Read() )
|
||||
{
|
||||
var tokenType = reader.TokenType;
|
||||
if ( tokenType == JsonToken.EndArray )
|
||||
break;
|
||||
|
||||
if ( tokenType != JsonToken.StartArray )
|
||||
continue;
|
||||
|
||||
var val = new object[headers.Count];
|
||||
var col = 0;
|
||||
|
||||
while ( reader.Read() )
|
||||
{
|
||||
tokenType = reader.TokenType;
|
||||
if ( tokenType == JsonToken.StartArray )
|
||||
continue;
|
||||
if ( tokenType == JsonToken.EndArray )
|
||||
break;
|
||||
|
||||
switch ( tokenType )
|
||||
{
|
||||
case JsonToken.Comment:
|
||||
case JsonToken.None:
|
||||
break;
|
||||
case JsonToken.Float:
|
||||
case JsonToken.String:
|
||||
case JsonToken.Boolean:
|
||||
case JsonToken.Date:
|
||||
case JsonToken.Null:
|
||||
case JsonToken.Integer:
|
||||
case JsonToken.Bytes:
|
||||
val[col] = reader.Value!;
|
||||
break;
|
||||
// case JsonToken.StartObject:
|
||||
// case JsonToken.StartArray:
|
||||
// case JsonToken.StartConstructor:
|
||||
// case JsonToken.PropertyName:
|
||||
// case JsonToken.Raw:
|
||||
// case JsonToken.Undefined:
|
||||
// case JsonToken.EndObject:
|
||||
// case JsonToken.EndConstructor:
|
||||
default:
|
||||
throw new SerializationException($"Unexpected token {tokenType}");
|
||||
}
|
||||
|
||||
col += 1;
|
||||
}
|
||||
|
||||
data.Add( val );
|
||||
}
|
||||
|
||||
ReadEndProperty( reader );
|
||||
return data;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanConvert(Type objectType) => typeof(ArrayBasedPivotData) == objectType;
|
||||
}
|
||||
|
||||
#endif
|
||||
157
Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataSerializer.cs
Normal file
157
Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataSerializer.cs
Normal file
@ -0,0 +1,157 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Runtime.Serialization;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.IO;
|
||||
using MongoDB.Bson.Serialization;
|
||||
using MongoDB.Bson.Serialization.Serializers;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core;
|
||||
|
||||
internal class ArrayBasedPivotDataSerializer : SerializerBase<ArrayBasedPivotData>
|
||||
{
|
||||
public static readonly DateTime UnixEpoch = new(
|
||||
1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public static ulong ToMillisecondsSinceUnixEpoch(DateTime dateTimeUtc) => (ulong)(dateTimeUtc - UnixEpoch).TotalMilliseconds;
|
||||
|
||||
public override void Serialize( BsonSerializationContext context, BsonSerializationArgs args, ArrayBasedPivotData value )
|
||||
{
|
||||
if ( value == null )
|
||||
throw new ArgumentNullException( nameof(value), "ArrayBasedPivotData instance is null" );
|
||||
if ( string.IsNullOrWhiteSpace( value.Id ) )
|
||||
throw new SerializationException( "Id is not set for ArrayBasedPivotData instance" );
|
||||
var headers = value.Headers;
|
||||
if ( headers.Count == 0 )
|
||||
throw new SerializationException("Headers count is zero for ArrayBasedPivotData instance");
|
||||
|
||||
|
||||
context.Writer.WriteStartDocument();
|
||||
context.Writer.WriteName("_id");
|
||||
context.Writer.WriteString(value.Id);
|
||||
context.Writer.WriteName("ExpireAt");
|
||||
context.Writer.WriteDateTime((long)ToMillisecondsSinceUnixEpoch(value.ExpireAt));
|
||||
|
||||
context.Writer.WriteName("Headers");
|
||||
|
||||
context.Writer.WriteStartArray();
|
||||
foreach ( var header in headers )
|
||||
context.Writer.WriteString( header );
|
||||
context.Writer.WriteEndArray();
|
||||
|
||||
context.Writer.WriteName( "data" );
|
||||
context.Writer.WriteStartArray();
|
||||
|
||||
for ( var row = 0; row < value.Count; row ++ )
|
||||
{
|
||||
context.Writer.WriteStartArray();
|
||||
|
||||
for ( var col = 0; col < headers.Count; col++ )
|
||||
{
|
||||
var data = value.Get( col, row );
|
||||
if ( data == null )
|
||||
{
|
||||
context.Writer.WriteNull();
|
||||
continue;
|
||||
}
|
||||
|
||||
var t = data.GetType();
|
||||
if (t == typeof(double))
|
||||
context.Writer.WriteDouble((double)data);
|
||||
else if (t == typeof(string))
|
||||
context.Writer.WriteString((string)data);
|
||||
else if (t == typeof(int))
|
||||
context.Writer.WriteInt32((int)data);
|
||||
else if (t == typeof(long))
|
||||
context.Writer.WriteInt64((long)data);
|
||||
else if (t == typeof(DateTime))
|
||||
context.Writer.WriteDateTime((long)ToMillisecondsSinceUnixEpoch((DateTime)data));
|
||||
else if (t == typeof(bool))
|
||||
context.Writer.WriteBoolean((bool)data);
|
||||
else
|
||||
context.Writer.WriteNull();
|
||||
}
|
||||
context.Writer.WriteEndArray();
|
||||
}
|
||||
|
||||
context.Writer.WriteEndArray();
|
||||
context.Writer.WriteEndDocument();
|
||||
}
|
||||
|
||||
public override ArrayBasedPivotData Deserialize( BsonDeserializationContext context, BsonDeserializationArgs args )
|
||||
{
|
||||
context.Reader.ReadStartDocument();
|
||||
|
||||
ExpectName(context, "_id");
|
||||
var id = context.Reader.ReadString();
|
||||
ExpectName(context, "ExpireAt");
|
||||
context.Reader.ReadDateTime();
|
||||
|
||||
ExpectName(context, "Headers");
|
||||
|
||||
context.Reader.ReadStartArray();
|
||||
var headers = new List<string>();
|
||||
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
|
||||
headers.Add( context.Reader.ReadString() );
|
||||
context.Reader.ReadEndArray();
|
||||
|
||||
var data = new ArrayBasedPivotData( headers ) { Id = id };
|
||||
|
||||
ExpectName(context, "data");
|
||||
|
||||
context.Reader.ReadStartArray();
|
||||
|
||||
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument) // rows
|
||||
{
|
||||
context.Reader.ReadStartArray();
|
||||
|
||||
var val = new object[headers.Count];
|
||||
var col = 0;
|
||||
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument) // columns
|
||||
{
|
||||
var v = BsonValueSerializer.Instance.Deserialize(context);
|
||||
|
||||
try
|
||||
{
|
||||
val[col] = BsonTypeMapper.MapToDotNetValue(v);
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
v[col] = null;
|
||||
}
|
||||
|
||||
col += 1;
|
||||
}
|
||||
context.Reader.ReadEndArray();
|
||||
|
||||
data.Add( val );
|
||||
}
|
||||
|
||||
context.Reader.ReadEndArray();
|
||||
context.Reader.ReadEndDocument();
|
||||
return data;
|
||||
}
|
||||
|
||||
private static void ExpectName( BsonDeserializationContext context, string elementExpected )
|
||||
{
|
||||
var name = context.Reader.ReadName();
|
||||
if ( name != elementExpected )
|
||||
throw new SerializationException( $"{elementExpected} expected, but got {name}" );
|
||||
}
|
||||
}
|
||||
37
Rms.Risk.Mango.Pivot.Core/Highlighting.cs
Normal file
37
Rms.Risk.Mango.Pivot.Core/Highlighting.cs
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public class Highlighting
|
||||
{
|
||||
public enum HighlightingMode
|
||||
{
|
||||
Off = 0,
|
||||
Breaks = 1,
|
||||
HeatMap = 2
|
||||
}
|
||||
|
||||
public HighlightingMode Mode { get; set; } = HighlightingMode.Off;
|
||||
|
||||
public double MinBound { get; set; } = double.MinValue;
|
||||
public double MaxBound { get; set; } = double.MaxValue;
|
||||
}
|
||||
266
Rms.Risk.Mango.Pivot.Core/IPivotTableDataSource.cs
Normal file
266
Rms.Risk.Mango.Pivot.Core/IPivotTableDataSource.cs
Normal file
@ -0,0 +1,266 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using Newtonsoft.Json;
|
||||
using Rms.Risk.Mango.Pivot.Core.Models;
|
||||
using System.Drawing;
|
||||
using System.Text.RegularExpressions;
|
||||
using static Rms.Risk.Mango.Pivot.Core.IPivotTableDataSource;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core;
|
||||
|
||||
public class PivotColumnDescriptor
|
||||
{
|
||||
[JsonIgnore] public Regex NameRegex { get; private set; } = null!;
|
||||
public Color Background { get; set; }
|
||||
public Color AlternateBackground { get; set; }
|
||||
public string Format { get; set; } = "";
|
||||
public bool ShowTotals { get; set; } = true;
|
||||
|
||||
public string NameRegexString
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
NameRegex = new(
|
||||
value,
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase |
|
||||
RegexOptions.Singleline
|
||||
);
|
||||
}
|
||||
} = "";
|
||||
}
|
||||
|
||||
public enum PivotFieldPurpose
|
||||
{
|
||||
Data = 0,
|
||||
PrimaryKey1 = 1,
|
||||
PrimaryKey2 = 2,
|
||||
Key = 3,
|
||||
Info = 4,
|
||||
Hidden = 5
|
||||
}
|
||||
|
||||
public class PivotFieldDescriptor
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public PivotFieldPurpose Purpose { get; set; }
|
||||
|
||||
[JsonIgnore] public Type Type { get; set; } = typeof(object);
|
||||
|
||||
public string TypeString
|
||||
{
|
||||
get => Type.Name;
|
||||
set => Type = Type.GetType( "System."+value ) ?? throw new ("Type not found: System."+value);
|
||||
}
|
||||
}
|
||||
|
||||
public enum CollectionType
|
||||
{
|
||||
All,
|
||||
NoMeta,
|
||||
HaveMeta
|
||||
}
|
||||
|
||||
public class GroupedPivot : ICloneable
|
||||
{
|
||||
public required string Text { get; init; }
|
||||
public bool IsGroup { get; init; }
|
||||
public required PivotDefinition Pivot { get; init; }
|
||||
|
||||
public override string ToString() => Text;
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
|
||||
public GroupedPivot Clone() =>
|
||||
new()
|
||||
{
|
||||
Text = Text,
|
||||
IsGroup = IsGroup,
|
||||
Pivot = Pivot.Clone()
|
||||
};
|
||||
}
|
||||
|
||||
public class GroupedCollection
|
||||
{
|
||||
public required string DataSourcePrefix { get; init; }
|
||||
public string CollectionNameWithPrefix => $"{DataSourcePrefix}: {CollectionNameWithoutPrefix}";
|
||||
public required string CollectionNameWithoutPrefix { get; init; }
|
||||
public bool IsGroup { get; init; }
|
||||
public PivotColumnDescriptor[] ColumnDescriptors { get; set; } = [];
|
||||
public HashSet<string> DataFields { get; set; } = [];
|
||||
public HashSet<string> KeyFields { get; set; } = [];
|
||||
public DateTime[] Cobs { get; set; } = [];
|
||||
public string[] Departments { get; set; } = [];
|
||||
public List<GroupedPivot> Pivots { get; set; } = [];
|
||||
public Dictionary<string, PivotFieldDescriptor> FieldTypes { get; set; } = [];
|
||||
|
||||
public override string ToString() => CollectionNameWithPrefix;
|
||||
|
||||
public void CopyFrom(GroupedCollection other)
|
||||
{
|
||||
if ( other == null ) throw new ArgumentNullException(nameof(other));
|
||||
|
||||
ColumnDescriptors = other.ColumnDescriptors;
|
||||
DataFields = other.DataFields;
|
||||
KeyFields = other.KeyFields;
|
||||
Cobs = other.Cobs;
|
||||
Departments = other.Departments;
|
||||
Pivots = other.Pivots;
|
||||
FieldTypes = other.FieldTypes;
|
||||
}
|
||||
|
||||
public List<string> GetDrilldownKeyFields(PivotFieldPurpose purpose) =>
|
||||
FieldTypes
|
||||
.Where( x => x.Value.Purpose == purpose )
|
||||
.Select(x => x.Key)
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public interface IPivotTableDataSource
|
||||
{
|
||||
string SourceId { get; }
|
||||
string Prefix { get; }
|
||||
|
||||
string User { get; set; }
|
||||
|
||||
Task<List<GroupedCollection>> GetAllMeta(bool force = false, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get drilldown formula for the given column
|
||||
/// </summary>
|
||||
/// <param name="collectionName"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value">Value to be compared with. Only records not matching this value will be shown.</param>
|
||||
/// <param name="equals">If true "name = value", if false "name != value"</param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<string> GetDrilldownAsync(string collectionName, string name, string value = "\"\"", bool equals = false, CancellationToken token = default );
|
||||
|
||||
/// <summary>
|
||||
/// Aggregate data
|
||||
/// </summary>
|
||||
/// <param name="collectionName"></param>
|
||||
/// <param name="def">Pivot definition</param>
|
||||
/// <param name="extraFilter">Extra $match stage</param>
|
||||
/// <param name="skipCache">Skip cached results</param>
|
||||
/// <param name="userName"></param>
|
||||
/// <param name="maxFetchSize"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns>Pivoted data</returns>
|
||||
Task<IPivotedData> PivotAsync(
|
||||
string collectionName,
|
||||
PivotDefinition def,
|
||||
FilterExpressionTree.ExpressionGroup? extraFilter,
|
||||
bool skipCache,
|
||||
string? userName = null,
|
||||
int maxFetchSize = -1,
|
||||
CancellationToken token = default
|
||||
);
|
||||
|
||||
public enum PivotType
|
||||
{
|
||||
Predefined,
|
||||
User,
|
||||
UserAndPredefined,
|
||||
All
|
||||
}
|
||||
|
||||
Task UpdatePredefinedPivotsAsync(string collectionName,
|
||||
IEnumerable<PivotDefinition> pivots,
|
||||
bool predefined = false,
|
||||
string? userName = null,
|
||||
CancellationToken token = default);
|
||||
|
||||
Task UpdatePivotAsync(string collectionName,
|
||||
PivotDefinition pivot,
|
||||
string? userName = null,
|
||||
CancellationToken token = default)
|
||||
=> UpdatePredefinedPivotsAsync(collectionName, [pivot], pivot.IsPredefined, userName, token);
|
||||
|
||||
/// <summary>
|
||||
/// Preprocess def to get proper query text.
|
||||
/// Should perform all sort of postprocessing appied to the query def.
|
||||
/// </summary>
|
||||
/// <param name="collectionName">mongo collection Name</param>
|
||||
/// <param name="def">Pivot definition</param>
|
||||
/// <param name="extraFilter">Extra $match stage</param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns>Processed query</returns>
|
||||
Task<string> GetQueryTextAsync(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default );
|
||||
|
||||
/// <summary>
|
||||
/// Get a single document
|
||||
/// </summary>
|
||||
/// <param name="collectionName">mongo collection Name</param>
|
||||
/// <param name="keys">Primary key fields in no particular order</param>
|
||||
/// <param name="extraFilter">Extra $match stage</param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns>Json string</returns>
|
||||
Task<string> GetDocumentAsync(string collectionName, KeyValuePair<string, object> [] keys, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a single document
|
||||
/// </summary>
|
||||
/// <param name="collectionName">mongo collection Name</param>
|
||||
/// <param name="filterText">Filter selecting a single document</param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns>Json string</returns>
|
||||
Task<string> GetDocumentAsync(string collectionName, FilterExpressionTree.ExpressionGroup filterText, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Delete pivot from the collection. Works for user pivots only.
|
||||
/// </summary>
|
||||
/// <param name="collectionName">mongo collection Name</param>
|
||||
/// <param name="pivotName">Pivot to delete</param>
|
||||
/// <param name="groupName">Group pivot belongs to. Currently only "User Pivots"</param>
|
||||
/// <param name="userName">User who owns the pivot</param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task DeletePivotAsync(string collectionName, string pivotName, string groupName, string userName, CancellationToken token = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Low level metadata access interface. Should not be used directly in the application. Intended to be used internal caches only. <see cref="PivotMetaCache"/>
|
||||
/// Applications should use <see cref="IPivotTableDataSource.GetAllMeta(bool, CancellationToken)"/> instead.
|
||||
/// </summary>
|
||||
public interface IPivotTableDataSourceMetaProvider
|
||||
{
|
||||
string SourceId { get; }
|
||||
string Prefix { get; }
|
||||
|
||||
Task<string[]> GetCollectionsAsync(CollectionType includeMeta = CollectionType.All, CancellationToken token = default);
|
||||
Task<string[]> GetDepartmentsAsync(string collectionName, CancellationToken token = default);
|
||||
Task<(string, string)[]> GetDesksWithDepartmentAsync(string collectionName, CancellationToken token = default);
|
||||
Task<string[]> GetKeyFieldsAsync(string collectionName, CancellationToken token = default);
|
||||
Task<string[]> GetDrilldownKeyFieldsAsync(string collectionName, PivotFieldPurpose keyLevel, CancellationToken token = default);
|
||||
Task<string[]> GetDataFieldsAsync(string collectionName, CancellationToken token = default);
|
||||
Task<PivotColumnDescriptor[]> GetColumnDescriptorsAsync(string collectionName, CancellationToken token = default);
|
||||
Task<string[]> GetCobDatesAsync(string collectionName, bool force = false,CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get all field types including keys, data and calculated fields
|
||||
/// </summary>
|
||||
/// <returns>Field types</returns>
|
||||
Dictionary<string, PivotFieldDescriptor> GetFieldTypes(string collectionName);
|
||||
|
||||
Task<List<PivotDefinition>> GetPivotsAsync(string collectionName, PivotType pivotType, string? userName = null, CancellationToken token = default);
|
||||
|
||||
}
|
||||
69
Rms.Risk.Mango.Pivot.Core/IPivotedData.cs
Normal file
69
Rms.Risk.Mango.Pivot.Core/IPivotedData.cs
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Pivot.Core;
|
||||
|
||||
public interface IPivotedData
|
||||
{
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// name : column
|
||||
/// </summary>
|
||||
IReadOnlyCollection<string> Headers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get column positions. Beware that duplicate column will be missing.
|
||||
/// Only first unique occurrence of column name will be counted.
|
||||
/// </summary>
|
||||
Dictionary<string, int> GetColumnPositions()
|
||||
{
|
||||
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// this is safer than calling ToDictionary as it handles duplicate headers
|
||||
foreach ( var (key, pos) in Headers.Select((x, i) => (Key: x, Value: i)) )
|
||||
{
|
||||
dict.TryAdd(key, pos);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Number of rows
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get element at (col, row)
|
||||
/// </summary>
|
||||
/// <param name="col"></param>
|
||||
/// <param name="row"></param>
|
||||
/// <returns></returns>
|
||||
object? Get( int col, int row );
|
||||
|
||||
Type GetColumnType( int col );
|
||||
|
||||
/// <summary>
|
||||
/// Create filtered copy of data. Filter func must return true if you want row to remain in the output copy.
|
||||
/// </summary>
|
||||
/// <param name="filter"></param>
|
||||
/// <returns></returns>
|
||||
IPivotedData Filter(Func<int, bool> filter);
|
||||
}
|
||||
35
Rms.Risk.Mango.Pivot.Core/Models/CalcFieldDef.cs
Normal file
35
Rms.Risk.Mango.Pivot.Core/Models/CalcFieldDef.cs
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public class CalcFieldDef(string formula, string drillDown, string[]? lookupDef, string aggregationOperator) : ICloneable
|
||||
{
|
||||
public string Formula { get; set; } = formula;
|
||||
public string DrillDown { get; set; } = drillDown;
|
||||
public string[] LookupDef { get; set; } = lookupDef ?? [];
|
||||
public string AggregationOperator { get; set; } = aggregationOperator;
|
||||
|
||||
public override string ToString() => $"{AggregationOperator} {Formula}";
|
||||
|
||||
object ICloneable. Clone() => Clone();
|
||||
public CalcFieldDef Clone() => (CalcFieldDef)MemberwiseClone();
|
||||
}
|
||||
89
Rms.Risk.Mango.Pivot.Core/Models/CollStatsModel.cs
Normal file
89
Rms.Risk.Mango.Pivot.Core/Models/CollStatsModel.cs
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class CollStatsModel
|
||||
{
|
||||
public bool Sharded { get; set; }
|
||||
public bool Capped { get; set; }
|
||||
public CollStatsData WiredTiger { get; set; } = new CollStatsData();
|
||||
public Dictionary<string, CollStatsData> IndexDetails { get; set; } = new();
|
||||
|
||||
// New fields added based on the provided JSON structure
|
||||
public string Ns { get; set; } = string.Empty;
|
||||
public long Count { get; set; }
|
||||
public long Size { get; set; }
|
||||
public long StorageSize { get; set; }
|
||||
public long TotalIndexSize { get; set; }
|
||||
public long TotalSize { get; set; }
|
||||
public Dictionary<string, long> IndexSizes { get; set; } = new();
|
||||
public double AvgObjSize { get; set; }
|
||||
public long MaxSize { get; set; }
|
||||
public int NIndexes { get; set; }
|
||||
public int ScaleFactor { get; set; }
|
||||
public int NChunks { get; set; }
|
||||
|
||||
public static CollStatsModel FromJson(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
return System.Text.Json.JsonSerializer.Deserialize<CollStatsModel>(json, options) ?? new();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Log the exception if necessary
|
||||
return new ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CollStatsData
|
||||
{
|
||||
public Metadata Metadata { get; set; } = new();
|
||||
public string CreationString { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
public string Uri { get; set; } = "";
|
||||
public Dictionary<string, ulong>? LSM { get; set; }
|
||||
public Dictionary<string, ulong>? Autocommit { get; set; }
|
||||
public Dictionary<string, ulong>? Backup { get; set; }
|
||||
public Dictionary<string, ulong>? BlockManager { get; set; }
|
||||
public Dictionary<string, ulong>? Btree { get; set; }
|
||||
public Dictionary<string, ulong>? Cache { get; set; }
|
||||
public Dictionary<string, ulong>? CacheWalk { get; set; }
|
||||
public Dictionary<string, ulong>? Checkpoint { get; set; }
|
||||
public Dictionary<string, ulong>? Compression { get; set; }
|
||||
public Dictionary<string, ulong>? Cursor { get; set; }
|
||||
public Dictionary<string, ulong>? Reconciliation { get; set; }
|
||||
public Dictionary<string, ulong>? Session { get; set; }
|
||||
public Dictionary<string, ulong>? Transaction { get; set; }
|
||||
}
|
||||
|
||||
public class Metadata
|
||||
{
|
||||
public int FormatVersion { get; set; }
|
||||
}
|
||||
47
Rms.Risk.Mango.Pivot.Core/Models/ColorConverter.cs
Normal file
47
Rms.Risk.Mango.Pivot.Core/Models/ColorConverter.cs
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Collections.Concurrent;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
internal class ColorConverter
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, Color> _colorConverter = new();
|
||||
|
||||
public static Color ConvertFromString( string colorStr )
|
||||
{
|
||||
if ( _colorConverter.TryGetValue( colorStr, out var color ) )
|
||||
return color;
|
||||
|
||||
if ( colorStr.StartsWith( "#" ) )
|
||||
{
|
||||
color = Color.FromArgb( int.Parse( colorStr[1..], NumberStyles.HexNumber ) );
|
||||
_colorConverter.TryAdd( colorStr, color );
|
||||
return color;
|
||||
}
|
||||
|
||||
color = Color.FromName( colorStr );
|
||||
if ( color.IsEmpty )
|
||||
color = Color.DimGray;
|
||||
_colorConverter.TryAdd( colorStr, color );
|
||||
return color;
|
||||
}
|
||||
}
|
||||
93
Rms.Risk.Mango.Pivot.Core/Models/DatabaseStatsModel.cs
Normal file
93
Rms.Risk.Mango.Pivot.Core/Models/DatabaseStatsModel.cs
Normal file
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace Rms.Risk.Mango.Services.Models;
|
||||
|
||||
public class DatabaseStatsModel
|
||||
{
|
||||
public Dictionary<string, DatabaseStatsRaw> Raw { get; set; } = new();
|
||||
|
||||
public static DatabaseStatsModel FromBson(BsonDocument res)
|
||||
{
|
||||
var model = new DatabaseStatsModel();
|
||||
|
||||
// Check if "raw" exists and is a document
|
||||
if (res.Contains("raw") && res["raw"].IsBsonDocument)
|
||||
{
|
||||
var rawDoc = res["raw"].AsBsonDocument;
|
||||
foreach (var element in rawDoc.Elements)
|
||||
{
|
||||
// Each element's value is a BsonDocument representing DatabaseStatsRaw
|
||||
if (element.Value.IsBsonDocument)
|
||||
{
|
||||
var statsRaw = DatabaseStatsRaw.FromBson(element.Value.AsBsonDocument);
|
||||
model.Raw[element.Name] = statsRaw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
public class DatabaseStatsRaw
|
||||
{
|
||||
public static DatabaseStatsRaw FromBson(BsonDocument doc) =>
|
||||
new()
|
||||
{
|
||||
Db = doc.GetValue("db", "").AsString,
|
||||
Collections = doc.GetValue("collections", 0).ToInt32(),
|
||||
Views = doc.GetValue("views", 0).ToInt32(),
|
||||
Objects = doc.GetValue("objects", 0L).ToInt64(),
|
||||
AvgObjSize = doc.GetValue("avgObjSize", 0.0).ToDouble(),
|
||||
DataSize = doc.GetValue("dataSize", 0.0).ToDouble(),
|
||||
StorageSize = doc.GetValue("storageSize", 0.0).ToDouble(),
|
||||
FreeStorageSize = doc.GetValue("freeStorageSize", 0.0).ToDouble(),
|
||||
Indexes = doc.GetValue("indexes", 0).ToInt32(),
|
||||
IndexSize = doc.GetValue("indexSize", 0.0).ToDouble(),
|
||||
IndexFreeStorageSize = doc.GetValue("indexFreeStorageSize", 0.0).ToDouble(),
|
||||
TotalSize = doc.GetValue("totalSize", 0.0).ToDouble(),
|
||||
TotalFreeStorageSize = doc.GetValue("totalFreeStorageSize", 0.0).ToDouble(),
|
||||
ScaleFactor = doc.GetValue("scaleFactor", 0).ToInt32(),
|
||||
FsUsedSize = doc.GetValue("fsUsedSize", 0.0).ToDouble(),
|
||||
FsTotalSize = doc.GetValue("fsTotalSize", 0.0).ToDouble(),
|
||||
Ok = doc.GetValue("ok", 0.0).ToDouble()
|
||||
};
|
||||
|
||||
public string Db { get; set; } = string.Empty;
|
||||
public int Collections { get; set; }
|
||||
public int Views { get; set; }
|
||||
public long Objects { get; set; }
|
||||
public double AvgObjSize { get; set; }
|
||||
public double DataSize { get; set; }
|
||||
public double StorageSize { get; set; }
|
||||
public double FreeStorageSize { get; set; }
|
||||
public int Indexes { get; set; }
|
||||
public double IndexSize { get; set; }
|
||||
public double IndexFreeStorageSize { get; set; }
|
||||
public double TotalSize { get; set; }
|
||||
public double TotalFreeStorageSize { get; set; }
|
||||
public int ScaleFactor { get; set; }
|
||||
public double FsUsedSize { get; set; }
|
||||
public double FsTotalSize { get; set; }
|
||||
public double Ok { get; set; }
|
||||
|
||||
|
||||
}
|
||||
379
Rms.Risk.Mango.Pivot.Core/Models/DrilldownSupport.cs
Normal file
379
Rms.Risk.Mango.Pivot.Core/Models/DrilldownSupport.cs
Normal file
@ -0,0 +1,379 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Reflection;
|
||||
using log4net;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Implements drilldown functionality.
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public class DrilldownSupport(List<GroupedCollection> _collections)
|
||||
{
|
||||
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!);
|
||||
|
||||
public const string DefaultDrilldownField = "<Default>";
|
||||
|
||||
public Func<KeyValuePair<string, object>[], Task> ShowDocument { get; set; } = _ => Task.CompletedTask;
|
||||
public Func<string, Task> MessageBoxShow { get; set; } = _ => Task.CompletedTask;
|
||||
public Func<string, string, Task<bool>> MessageBoxShowYesNo { get; set; } = (_,_) => Task.FromResult(false);
|
||||
public Func<Exception, Task> ShowException { get; set; } = _ => Task.CompletedTask;
|
||||
public Func<string, string, PivotDefinition?> GetPivotDefinition { get; set; } = (_,_) => null;
|
||||
public Func<string, string, Task<Tuple<string,PivotDefinition>?>> GetCustomDrilldown { get; set; } = (_,_) => Task.FromResult<Tuple<string,PivotDefinition>?>(null);
|
||||
|
||||
private GroupedCollection GetCollection(string name) => _collections.FirstOrDefault( x => x.CollectionNameWithPrefix == name ) ?? throw new ApplicationException($"Collection=\"{name}\" is not found");
|
||||
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
||||
public static bool ShowAllKeysInDrilldowns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines collection and pivot that needs to be executed.
|
||||
/// Returned pivot contains he filter required to show only rows of the original set that
|
||||
/// made contribution to the number shown in column/>.
|
||||
/// </summary>
|
||||
/// <param name="source">Data source</param>
|
||||
/// <param name="collectionName">Source collection name</param>
|
||||
/// <param name="displayName">Column name as displayed to drilldown to</param>
|
||||
/// <param name="displayToRealNameMap">Display name to real column name map</param>
|
||||
/// <param name="allColumns">All columns currently shown. Names must be resolvable via getValue</param>
|
||||
/// <param name="current">Currently shown pivot definition</param>
|
||||
/// <param name="getValue">Get value of any column for the current row</param>
|
||||
/// <param name="allDataFields">List of all data fields that can potentially be shown using current pivot</param>
|
||||
/// <param name="allKeyFields">List of all key fields that can potentially be shown using current pivot</param>
|
||||
/// <returns>Collection name and pivot definition implementing drilldown for supplied field</returns>
|
||||
public async Task<Tuple<string, PivotDefinition>?> Drilldown(
|
||||
IPivotTableDataSource source,
|
||||
string collectionName,
|
||||
string displayName,
|
||||
Dictionary<string, string> displayToRealNameMap,
|
||||
string [] allColumns,
|
||||
PivotDefinition current,
|
||||
Func<string, object?> getValue,
|
||||
IReadOnlySet<string> allDataFields,
|
||||
IReadOnlySet<string> allKeyFields
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fields = GetPrimaryKey2KeyFields(collectionName, getValue, allColumns).ToArray();
|
||||
if ( fields.Length == GetCollection(collectionName).FieldTypes.Count( x => x.Value.Purpose == PivotFieldPurpose.PrimaryKey2 ) )
|
||||
{
|
||||
// all keys already shown. now ony one document can be selected.
|
||||
// show the detailed single document view instead of report
|
||||
|
||||
await ShowDocument( fields! );
|
||||
return null;
|
||||
}
|
||||
|
||||
var realName = Rename(displayName, displayToRealNameMap);
|
||||
|
||||
var pivotTuple = await GetCustomDrilldown( collectionName, realName );
|
||||
|
||||
pivotTuple ??= await TryCustomDrilldown(source, collectionName, current, getValue, realName, allColumns);
|
||||
|
||||
if ( pivotTuple != null )
|
||||
return pivotTuple;
|
||||
|
||||
// aggregation drilldown
|
||||
|
||||
// there is no reason to check this for map/reduce reports as they always contains unique column names
|
||||
if ( !allDataFields.Contains( realName ) && !allKeyFields.Contains( realName ) )
|
||||
{
|
||||
await MessageBoxShow( $"Drilldown is not supported for Column=\"{realName}\" DisplayedAs=\"{displayName}\"" );
|
||||
return null;
|
||||
}
|
||||
|
||||
// make inverted dictionary
|
||||
var realToDisplayNameMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach ( var item in displayToRealNameMap )
|
||||
realToDisplayNameMap[item.Value] = item.Key;
|
||||
|
||||
pivotTuple = await DrilldownInternal(source, collectionName, getValue, displayName, realName, realToDisplayNameMap, current, allKeyFields);
|
||||
return pivotTuple;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ShowException(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Rename(string name, Dictionary<string, string> map)
|
||||
{
|
||||
if ( !map.TryGetValue(name, out var value) || value == null )
|
||||
value = name;
|
||||
return value;
|
||||
}
|
||||
|
||||
private async Task<Tuple<string, PivotDefinition>?> DrilldownInternal(
|
||||
IPivotTableDataSource dataSource,
|
||||
string collectionName,
|
||||
Func<string, object?> getValue,
|
||||
string displayName,
|
||||
string realName,
|
||||
Dictionary<string, string> realToDisplayNameMap,
|
||||
PivotDefinition source,
|
||||
IReadOnlySet<string> allKeys)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = getValue(displayName);
|
||||
var isString = value is string;
|
||||
var isDate = value is DateTime;
|
||||
var isSql = collectionName.StartsWith("BFG: ");
|
||||
|
||||
var keyFields = source.KeyFields.ToArray();
|
||||
var dataFields = source.DataFields.ToArray();
|
||||
|
||||
var current = source.Clone();
|
||||
|
||||
current.Group = PivotDefinition.CurrentPivotGroup;
|
||||
|
||||
var keyFilter = "";
|
||||
|
||||
var fieldTypes = GetCollection(collectionName).FieldTypes;
|
||||
|
||||
foreach ( var keyColumn in keyFields )
|
||||
{
|
||||
var v = getValue( Rename(keyColumn.Replace( ".", " " ), realToDisplayNameMap) );
|
||||
var val = v == null
|
||||
? "null"
|
||||
: $"\"{v}\"";
|
||||
|
||||
if ( fieldTypes.TryGetValue( keyColumn, out var desc ) )
|
||||
{
|
||||
if ( desc.Type == typeof(DateTime) && v?.GetType() == typeof(DateTime) )
|
||||
val = isSql ? $"'{v:yyyy-MM-dd}'" : $"ISODate(\"{v:yyyy-MM-ddTHH:mm:ss.fff}Z\")";
|
||||
else if ( desc.Type != typeof(string) )
|
||||
val = val.Trim('\"');
|
||||
}
|
||||
|
||||
if ( keyFilter != "" )
|
||||
keyFilter += GetFilterConcatenationString(collectionName);
|
||||
|
||||
//keyFilter += $"{{ \"{keyColumn}\": {val} }}";
|
||||
keyFilter += await dataSource.GetDrilldownAsync(collectionName, keyColumn, val, true );
|
||||
}
|
||||
|
||||
var nullValue = isString
|
||||
? "\"\""
|
||||
: isDate
|
||||
? "null"
|
||||
: "0.0";
|
||||
|
||||
if ( keyFilter != "" )
|
||||
keyFilter += GetFilterConcatenationString(collectionName);
|
||||
|
||||
keyFilter += await dataSource.GetDrilldownAsync(collectionName, realName, nullValue );
|
||||
current.DrilldownFilter = keyFilter;
|
||||
|
||||
var keys1 = GetCollection(collectionName).GetDrilldownKeyFields(PivotFieldPurpose.PrimaryKey1);
|
||||
var keys2 = GetCollection(collectionName).GetDrilldownKeyFields(PivotFieldPurpose.PrimaryKey2);
|
||||
|
||||
var keys = (keys1.All(x => keyFields.Contains(x)) ? keys2 : keys1 )
|
||||
.Concat(
|
||||
ShowAllKeysInDrilldowns
|
||||
? allKeys
|
||||
: keyFields )
|
||||
.Distinct()
|
||||
.Where( allKeys.Contains )
|
||||
;
|
||||
|
||||
current.KeyFields = keys.ToArray();
|
||||
current.DataFields = dataFields;
|
||||
|
||||
_log.Debug( $"Drilldown Pivot=\"{source.Name}\" Filter:\n{current.Filter} DrilldownFilter:\n{current.DrilldownFilter}");
|
||||
|
||||
return Tuple.Create(collectionName, current);
|
||||
}
|
||||
catch ( Exception ex )
|
||||
{
|
||||
await ShowException( ex );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFilterConcatenationString(string collectionName) => collectionName.StartsWith("Forge:") ? "," : "\n\tAND ";
|
||||
|
||||
private async Task<Tuple<string, PivotDefinition>?> TryCustomDrilldown(
|
||||
IPivotTableDataSource dataSource,
|
||||
string collectionName,
|
||||
PivotDefinition? origPivot,
|
||||
Func<string, object?> getValue,
|
||||
string column,
|
||||
string[] allHeaders)
|
||||
{
|
||||
// try to find custom drilldown
|
||||
|
||||
var rec = origPivot?.Drilldown?.FirstOrDefault( x => x.ColumnName == column )
|
||||
?? origPivot?.Drilldown?.FirstOrDefault( x => x.ColumnName == DefaultDrilldownField )
|
||||
;
|
||||
if ( rec == null )
|
||||
return null;
|
||||
|
||||
// detect other keys shown
|
||||
var keyHeaders = GetCollection(collectionName).KeyFields;
|
||||
var keys = allHeaders
|
||||
.Where( x => keyHeaders.Contains( x ) )
|
||||
.Select( x => new KeyValuePair<string, string>(x, getValue( x )?.ToString() ?? "") )
|
||||
;
|
||||
|
||||
var basePivot = GetPivotDefinition( rec.DrilldownPivot, collectionName );
|
||||
|
||||
if ( basePivot != null )
|
||||
{
|
||||
var pivot = await SameCollectionDrilldown( dataSource, collectionName, origPivot, getValue, column, allHeaders, basePivot, rec, keys );
|
||||
return Tuple.Create(collectionName, pivot);
|
||||
}
|
||||
|
||||
var path = rec.DrilldownPivot.Split( '/' );
|
||||
if ( path.Length == 2 )
|
||||
{
|
||||
if ( !await MessageBoxShowYesNo(
|
||||
$"Do you want to start separate instance and run \"{path[1]}\" for collection \"{path[0]}\" in order to drill down to \"{column}\"?",
|
||||
"Drill down" ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var tuple = await SeparateCollectionDrilldown( origPivot, getValue, column, allHeaders, keys, path[0], path[1] );
|
||||
return tuple;
|
||||
}
|
||||
|
||||
await MessageBoxShow( $"DrilldownPivot=\"{rec.DrilldownPivot}\" is not found. Column=\"{column}\"" );
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<Tuple<string, PivotDefinition>?> SeparateCollectionDrilldown(
|
||||
PivotDefinition? orig,
|
||||
Func<string, object?> getValue,
|
||||
string column,
|
||||
string [] allHeaders,
|
||||
IEnumerable<KeyValuePair<string, string>> keys,
|
||||
string destCollection,
|
||||
string destPivotName
|
||||
)
|
||||
{
|
||||
var destPivot = GetPivotDefinition(destPivotName, destCollection)?.Clone();
|
||||
|
||||
var rec = orig?.Drilldown.FirstOrDefault( x => x.ColumnName == column )
|
||||
?? orig?.Drilldown.FirstOrDefault( x => x.ColumnName == DefaultDrilldownField )
|
||||
;
|
||||
|
||||
if (rec == null || destPivot == null)
|
||||
{
|
||||
await MessageBoxShow($"Drilldown is not configured for \"{column}\"");
|
||||
return null;
|
||||
}
|
||||
|
||||
destPivot.DrilldownFilter = string.Concat(keys.Select(x => $"{{ \"{x.Key}\" : \"{x.Value}\" }}, ")) // other keys
|
||||
+ PrepareDrilldownCondition(allHeaders, rec.DrilldownCondition, column, getValue); // drilldown condition
|
||||
|
||||
return Tuple.Create(destCollection, destPivot);
|
||||
}
|
||||
|
||||
private Task<PivotDefinition> SameCollectionDrilldown(
|
||||
IPivotTableDataSource dataSource,
|
||||
string collectionName,
|
||||
PivotDefinition? origPivot,
|
||||
Func<string, object?> getValue,
|
||||
string column,
|
||||
string [] allHeaders,
|
||||
PivotDefinition basePivot,
|
||||
PivotDefinition.DrilldownDef rec,
|
||||
IEnumerable<KeyValuePair<string, string>> keys
|
||||
)
|
||||
{
|
||||
var drill = basePivot.Clone();
|
||||
|
||||
drill.DrilldownFilter = string.Concat(keys.Select(x => $"{{ \"{x.Key}\" : \"{x.Value}\" }}, ")) // other keys
|
||||
+ PrepareDrilldownCondition(allHeaders, rec.DrilldownCondition, column, getValue); // drilldown condition
|
||||
|
||||
drill.Name = PivotDefinition.CurrentPivotName;
|
||||
drill.Group = PivotDefinition.CurrentPivotGroup;
|
||||
|
||||
if ( !string.IsNullOrWhiteSpace( rec.AppendToBeforeGrouping ) )
|
||||
{
|
||||
// append to "Before Grouping"
|
||||
if ( !string.IsNullOrWhiteSpace( drill.BeforeGrouping ) )
|
||||
drill.BeforeGrouping += ",\n";
|
||||
drill.BeforeGrouping += PrepareDrilldownCondition( allHeaders, rec.AppendToBeforeGrouping, column, getValue );
|
||||
}
|
||||
|
||||
var primaryKeys1 = GetCollection(collectionName).FieldTypes.Values
|
||||
.Where( x => x.Purpose == PivotFieldPurpose.PrimaryKey1 )
|
||||
.Select( x => x.Name )
|
||||
.ToList();
|
||||
|
||||
if ( origPivot?.KeyFields.Intersect( primaryKeys1 ).Count() != primaryKeys1.Count
|
||||
) // not all PK1 shown => add missing PK1
|
||||
{
|
||||
drill.KeyFields = drill.KeyFields.Concat( primaryKeys1.Where( x => !drill.KeyFields.Contains( x ) ) ).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// do the same for PK2
|
||||
var primaryKeys2 = GetCollection(collectionName).FieldTypes.Values
|
||||
.Where( x => x.Purpose == PivotFieldPurpose.PrimaryKey2 )
|
||||
.Select( x => x.Name )
|
||||
.ToList();
|
||||
|
||||
if ( origPivot.KeyFields.Intersect( primaryKeys2 ).Count() != primaryKeys2.Count
|
||||
) // not all PK2 selected => add missing PK1 and PK2
|
||||
{
|
||||
drill.KeyFields = drill.KeyFields.Concat( primaryKeys1.Where( x => !drill.KeyFields.Contains( x ) ) )
|
||||
.ToArray();
|
||||
drill.KeyFields = drill.KeyFields.Concat( primaryKeys2.Where( x => !drill.KeyFields.Contains( x ) ) )
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(drill);
|
||||
}
|
||||
|
||||
|
||||
private IEnumerable<KeyValuePair<string, object?>> GetPrimaryKey2KeyFields(
|
||||
string collectionName,
|
||||
Func<string, object?> getValue,
|
||||
string [] allHeaders
|
||||
) =>
|
||||
GetCollection(collectionName).FieldTypes
|
||||
.Where( x => x.Value.Purpose == PivotFieldPurpose.PrimaryKey2 )
|
||||
.Select( x => x.Key )
|
||||
.Where( allHeaders.Contains )
|
||||
.Select( key => new KeyValuePair<string, object?>( key, getValue( key ) ) );
|
||||
|
||||
private static string PrepareDrilldownCondition(
|
||||
string [] allHeaders,
|
||||
string cond,
|
||||
string columnName,
|
||||
Func<string, object?> getValue
|
||||
)
|
||||
{
|
||||
cond = allHeaders
|
||||
.Aggregate(
|
||||
cond,
|
||||
(current, header) => current.Replace($"<{header}>", getValue(header)?.ToString())
|
||||
);
|
||||
|
||||
cond = cond.Replace("<COLNAME>", columnName);
|
||||
return cond;
|
||||
}
|
||||
}
|
||||
209
Rms.Risk.Mango.Pivot.Core/Models/ExpiringConcurrentDictionary.cs
Normal file
209
Rms.Risk.Mango.Pivot.Core/Models/ExpiringConcurrentDictionary.cs
Normal file
@ -0,0 +1,209 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A thread-safe dictionary with elements that expire after a specified duration.
|
||||
/// Accessing an element renews its expiration time.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the values in the dictionary.</typeparam>
|
||||
public class ExpiringConcurrentDictionary<TKey, TValue> : IDisposable where TValue : class where TKey : notnull
|
||||
{
|
||||
private readonly ConcurrentDictionary<TKey, (TValue Value, DateTime Expiration)> _dictionary = new();
|
||||
private readonly TimeSpan _expirationDuration;
|
||||
private readonly bool _shouldDispose;
|
||||
private readonly Func<TKey, TValue>? _elementFactory;
|
||||
private readonly Timer _cleanupTimer;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExpiringConcurrentDictionary{TKey, TValue}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="expirationDuration">The duration after which elements expire.</param>
|
||||
/// <param name="elementFactory">The factory method to create elements for missing keys.</param>
|
||||
/// <param name="cleanupInterval">The interval at which expired elements are removed.</param>
|
||||
/// <param name="shouldDispose">Indicates whether elements should be disposed when removed.</param>
|
||||
public ExpiringConcurrentDictionary(Func<TKey, TValue> elementFactory, TimeSpan expirationDuration, TimeSpan cleanupInterval, bool shouldDispose = true)
|
||||
: this(expirationDuration, cleanupInterval, shouldDispose)
|
||||
{
|
||||
_elementFactory = elementFactory ?? throw new ArgumentNullException(nameof(elementFactory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExpiringConcurrentDictionary{TKey, TValue}"/> class.
|
||||
/// Element factory must be provided later via <see cref="GetOrAdd(TKey, Func{TKey, TValue}?)"/>.
|
||||
/// </summary>
|
||||
/// <param name="expirationDuration">The duration after which elements expire.</param>
|
||||
/// <param name="cleanupInterval">The interval at which expired elements are removed.</param>
|
||||
/// <param name="shouldDispose">Indicates whether elements should be disposed when removed.</param>
|
||||
public ExpiringConcurrentDictionary(TimeSpan expirationDuration, TimeSpan cleanupInterval, bool shouldDispose = true)
|
||||
{
|
||||
_expirationDuration = expirationDuration;
|
||||
_shouldDispose = shouldDispose;
|
||||
_elementFactory = null;
|
||||
_cleanupTimer = new(_ => RemoveExpiredElements(), null, cleanupInterval, cleanupInterval);
|
||||
}
|
||||
|
||||
~ExpiringConcurrentDictionary()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or adds an element by key. If the element exists and is not expired, its expiration is renewed.
|
||||
/// If the element does not exist or is expired, a new element is created using the factory method.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element.</param>
|
||||
/// <param name="elementFactory">The factory method to create the element if it does not exist or is expired.</param>
|
||||
/// <returns>The element associated with the key.</returns>
|
||||
public TValue GetOrAdd(TKey key, Func<TKey, TValue>? elementFactory = null)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (_dictionary.TryGetValue(key, out var entry))
|
||||
{
|
||||
if (entry.Expiration > now)
|
||||
{
|
||||
// Renew expiration and return the value
|
||||
_dictionary[key] = (entry.Value, now.Add(_expirationDuration));
|
||||
return entry.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove expired entry
|
||||
RemoveEntry(key, entry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new value using the factory method
|
||||
var factory = elementFactory ?? _elementFactory;
|
||||
if (factory == null)
|
||||
{
|
||||
throw new InvalidOperationException("Element factory is not specified.");
|
||||
}
|
||||
var newValue = factory(key);
|
||||
var newEntry = (newValue, now.Add(_expirationDuration));
|
||||
if (_dictionary.TryAdd(key, newEntry))
|
||||
{
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
var values = _dictionary.Values.ToList();
|
||||
_dictionary.Clear();
|
||||
|
||||
foreach (var entry in values)
|
||||
{
|
||||
DisposeIfNecessary(entry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes expired elements from the dictionary.
|
||||
/// If elements implement <see cref="IDisposable"/>, they are disposed upon removal.
|
||||
/// </summary>
|
||||
public void RemoveExpiredElements()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var key in _dictionary.Keys)
|
||||
{
|
||||
if (_dictionary.TryGetValue(key, out var entry) && entry.Expiration <= now)
|
||||
{
|
||||
RemoveEntry(key, entry.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an element by key.
|
||||
/// If the element implements <see cref="IDisposable"/>, it is disposed upon removal.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to remove.</param>
|
||||
/// <returns>True if the element was removed; otherwise, false.</returns>
|
||||
public bool TryRemove(TKey key)
|
||||
{
|
||||
if (_dictionary.TryRemove(key, out var entry))
|
||||
{
|
||||
DisposeIfNecessary(entry.Value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the dictionary and its elements if they implement <see cref="IDisposable"/>.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_cleanupTimer.Dispose();
|
||||
foreach (var entry in _dictionary.Values)
|
||||
{
|
||||
DisposeIfNecessary(entry.Value);
|
||||
}
|
||||
_dictionary.Clear();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEntry(TKey key, TValue value)
|
||||
{
|
||||
if (_dictionary.TryRemove(key, out _))
|
||||
{
|
||||
DisposeIfNecessary(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeIfNecessary(TValue value)
|
||||
{
|
||||
if (_shouldDispose && value is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Determines whether the dictionary contains the specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to locate in the dictionary.</param>
|
||||
/// <returns>True if the dictionary contains the key; otherwise, false.</returns>
|
||||
public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of elements in the dictionary.
|
||||
/// </summary>
|
||||
public int Count => _dictionary.Count;
|
||||
}
|
||||
110
Rms.Risk.Mango.Pivot.Core/Models/ExpiringObjectPool.cs
Normal file
110
Rms.Risk.Mango.Pivot.Core/Models/ExpiringObjectPool.cs
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class ExpiringObjectPool<TKey, TValue, TArg>
|
||||
where TValue : class
|
||||
where TKey : notnull
|
||||
{
|
||||
public static TimeSpan DefaultExpiryDuration = TimeSpan.FromHours(3);
|
||||
public static TimeSpan DefaultCleanupInterval = TimeSpan.FromMinutes(5);
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, (TValue Item, DateTime Expiry)> _pool;
|
||||
private readonly Func<TKey, TArg, CancellationToken, Task<TValue>> _objectGenerator;
|
||||
private readonly TimeSpan _expiryDuration;
|
||||
private readonly Timer _cleanupTimer;
|
||||
private readonly ConcurrentDictionary<TKey, Task<TValue>> _loadingTasks = new();
|
||||
|
||||
public ExpiringObjectPool(Func<TKey, TArg, CancellationToken, Task<TValue>> objectGenerator, TimeSpan expiryDuration = default, TimeSpan cleanupInterval = default )
|
||||
{
|
||||
if ( cleanupInterval == TimeSpan.Zero )
|
||||
cleanupInterval = DefaultCleanupInterval;
|
||||
if (expiryDuration == TimeSpan.Zero )
|
||||
expiryDuration = DefaultExpiryDuration;
|
||||
|
||||
_objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator));
|
||||
_expiryDuration = expiryDuration;
|
||||
_pool = new ConcurrentDictionary<TKey, (TValue, DateTime)>();
|
||||
|
||||
// Set up a timer to clean up expired objects
|
||||
_cleanupTimer = new Timer(CleanupExpiredObjects, null, cleanupInterval, cleanupInterval);
|
||||
}
|
||||
|
||||
public void Clear() => _pool.Clear();
|
||||
|
||||
public async Task<TValue> Get(TKey key, TArg arg, CancellationToken token = default)
|
||||
{
|
||||
if (_pool.TryGetValue(key, out var entry) && entry.Expiry > DateTime.UtcNow)
|
||||
{
|
||||
return entry.Item;
|
||||
}
|
||||
|
||||
// Check if the key is already being loaded
|
||||
var loadingTask = _loadingTasks.GetOrAdd(key, _ => LoadNewItem(key, arg, token));
|
||||
|
||||
try
|
||||
{
|
||||
var newItem = await loadingTask;
|
||||
return newItem;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Remove the task once loading is complete
|
||||
_loadingTasks.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TValue> LoadNewItem(TKey key, TArg arg, CancellationToken token)
|
||||
{
|
||||
var newItem = await _objectGenerator(key, arg, token);
|
||||
_pool[key] = (newItem, DateTime.UtcNow.Add(_expiryDuration));
|
||||
return newItem;
|
||||
}
|
||||
|
||||
public void ReturnObject(TKey key, TValue item)
|
||||
{
|
||||
if (item == null) throw new ArgumentNullException(nameof(item));
|
||||
|
||||
_pool[key] = (item, DateTime.UtcNow.Add(_expiryDuration));
|
||||
}
|
||||
|
||||
private void CleanupExpiredObjects(object? state)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Remove expired objects
|
||||
var expiredKeys = _pool.Where(pair => pair.Value.Expiry <= now).Select(pair => pair.Key).ToList();
|
||||
foreach (var key in expiredKeys)
|
||||
{
|
||||
_pool.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cleanupTimer.Dispose();
|
||||
}
|
||||
|
||||
public void Remove(TKey key)
|
||||
{
|
||||
_pool.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
168
Rms.Risk.Mango.Pivot.Core/Models/FieldMapping.cs
Normal file
168
Rms.Risk.Mango.Pivot.Core/Models/FieldMapping.cs
Normal file
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class FieldMapping
|
||||
{
|
||||
public const double DrilldownTolerance = 0.000001;
|
||||
|
||||
public FieldMappingData Data { get; set; } = new();
|
||||
|
||||
public int Count => Data.Fields.Count;
|
||||
public bool UseMapping => Data.UseMapping;
|
||||
public IEnumerable<string> FieldNames => Data.Fields.Keys;
|
||||
public IEnumerable<KeyValuePair<string, SingleFieldMapping>> Fields => Data.Fields;
|
||||
public IEnumerable<string> CalculatedFields => Data.CalculatedFields.Keys;
|
||||
|
||||
public override string ToString() => $"Fields={Count} CalculatedFields={Data.CalculatedFields.Count} Lookups={Data.Lookups.Count}";
|
||||
|
||||
public bool TryGetValue( string name, out SingleFieldMapping? mapping ) => Data.Fields.TryGetValue( name, out mapping );
|
||||
|
||||
public SingleFieldMapping this[ string name ]
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( !Data.Fields.TryGetValue( name, out var mapping )
|
||||
&& !Data.Fields.TryGetValue( name.Replace( ".", " " ), out mapping ) )
|
||||
throw new MissingFieldException( $"Field \"{name}\" is not found" );
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
set => Data.Fields[name] = value;
|
||||
}
|
||||
|
||||
public bool ContainsKey( string name ) =>
|
||||
Data.Fields.ContainsKey( name )
|
||||
|| Data.Fields.ContainsKey( name.Replace( ".", " " ) );
|
||||
|
||||
public string MapField(string name)
|
||||
{
|
||||
if (!Data.UseMapping)
|
||||
return name;
|
||||
|
||||
return !TryGetValue(name, out var m)
|
||||
? name
|
||||
: $"f{m?.Id}";
|
||||
}
|
||||
|
||||
public string UnmapField(string name)
|
||||
{
|
||||
if (!Data.UseMapping || !name.StartsWith("f"))
|
||||
return name;
|
||||
if (!int.TryParse(name[1..], out var id) || id <= 0)
|
||||
return name;
|
||||
var n = Data.Fields.FirstOrDefault(x => x.Value.Id == id).Key;
|
||||
return n ?? name;
|
||||
}
|
||||
|
||||
public FieldMapping(bool use)
|
||||
{
|
||||
Data.UseMapping = use;
|
||||
}
|
||||
|
||||
public void MapAllFields(IList<BsonDocument> pipeline)
|
||||
{
|
||||
for (var i = 0; i < pipeline.Count; i++)
|
||||
{
|
||||
var stageName = pipeline[i].Elements.First().Name;
|
||||
pipeline[i] = MapAllFields(pipeline[i], stageName == "$project" || stageName == "$match");
|
||||
}
|
||||
}
|
||||
|
||||
public BsonDocument MapAllFields(BsonDocument bsonDocument, bool replaceNakedNames)
|
||||
{
|
||||
var json = bsonDocument.ToJson();
|
||||
|
||||
json = MapAllFields(json, replaceNakedNames);
|
||||
|
||||
return BsonDocument.Parse(json);
|
||||
}
|
||||
|
||||
public string MapAllFields(string json, bool replaceNakedNames)
|
||||
{
|
||||
// ordering by key.Length to resolve conflicts like "TradePV" vs "PV"
|
||||
|
||||
json = Data.CalculatedFields.OrderBy(x => -x.Key.Length).Aggregate(json, (current, m) => current.Replace($"\"${m.Key}\"", m.Value.Formula));
|
||||
if (Data.UseMapping)
|
||||
json = Data.Fields.OrderBy(x => -x.Key.Length).Aggregate(json, (current, m) => current.Replace($"\"${m.Key}\"", $"\"$f{m.Value.Id}\""));
|
||||
if ( !replaceNakedNames )
|
||||
return json;
|
||||
|
||||
json = Data.CalculatedFields.OrderBy(x => -x.Key.Length).Aggregate(json, (current, m) => current.Replace($"\"{m.Key}\"", m.Value.Formula));
|
||||
if (Data.UseMapping)
|
||||
json = Data.Fields.OrderBy(x => -x.Key.Length).Aggregate(json, (current, m) => current.Replace($"\"{m.Key}\"", $"\"f{m.Value.Id}\""));
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
public void ClearCalculatedFields() => Data.CalculatedFields.Clear();
|
||||
|
||||
public void AddCalculatedField(string name, string formula, string drillDown, string []? lookupDef = null, string aggregationOperator = "$sum") => Data.CalculatedFields[name] = new(formula, drillDown, lookupDef, aggregationOperator);
|
||||
|
||||
public bool IsCalculated(string name) => Data.CalculatedFields.ContainsKey(name);
|
||||
|
||||
public string GetDrilldown(string column, string value = "\"\"", bool equals = false)
|
||||
{
|
||||
if ( Data.CalculatedFields.TryGetValue(column, out var field) )
|
||||
return field.DrillDown;
|
||||
|
||||
Data.Fields.TryGetValue(column, out var mapping);
|
||||
var isDouble = mapping != null && (mapping.Type == typeof(double) || mapping.Type == typeof(float) || mapping.Type == typeof(decimal));
|
||||
|
||||
var name = Data.Fields.ContainsKey( column.Replace(" ", ".") )
|
||||
? column.Replace(" ", ".")
|
||||
: column;
|
||||
|
||||
if (equals)
|
||||
{
|
||||
//TODO: this compares all double with ==. This may not be ideal, but I don't know how to implement abs(field -value) < TOLERANCE in $filter step
|
||||
return $"{{ \"{name}\": {value} }}"; // all the rest including integers and strings
|
||||
}
|
||||
|
||||
return isDouble
|
||||
? $"{{ \"$or\" : [ {{ \"{name}\": {{ \"$lte\" : {-DrilldownTolerance} }} }}, {{ \"{name}\": {{ \"$gte\" : {DrilldownTolerance} }} }} ] }}" // double = special
|
||||
: $"{{ \"{name}\": {{ \"$ne\" : {value} }} }}"; // all the rest including integers and strings
|
||||
}
|
||||
|
||||
public string GetLookup(string name)
|
||||
{
|
||||
var lookups = Data.CalculatedFields[name].LookupDef
|
||||
.Where(Data.Lookups.ContainsKey)
|
||||
.Distinct()
|
||||
.Select(x => Data.Lookups[x])
|
||||
;
|
||||
|
||||
return IsCalculated( name )
|
||||
? string.Join( ", " , lookups)
|
||||
: ""
|
||||
;
|
||||
}
|
||||
|
||||
public string GetFormula(string name) => Data.CalculatedFields[name].Formula;
|
||||
|
||||
public string GetAggregationOperator(string name) =>
|
||||
IsCalculated(name)
|
||||
? Data.CalculatedFields[name].AggregationOperator
|
||||
: "$sum";
|
||||
|
||||
public void AddLookup( string elem, string stages ) => Data.Lookups[elem] = stages;
|
||||
}
|
||||
97
Rms.Risk.Mango.Pivot.Core/Models/FieldMappingData.cs
Normal file
97
Rms.Risk.Mango.Pivot.Core/Models/FieldMappingData.cs
Normal file
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public class FieldMappingData : ICloneable
|
||||
{
|
||||
public DateTime CachedAt { get; set; }
|
||||
public bool UseMapping { get; set; }
|
||||
|
||||
public Dictionary<string, SingleFieldMapping> Fields { get; set; } = new( StringComparer.OrdinalIgnoreCase );
|
||||
public Dictionary<string, CalcFieldDef> CalculatedFields { get; set; } = new( StringComparer.OrdinalIgnoreCase );
|
||||
public Dictionary<string, string> Lookups { get; set; } = new( StringComparer.OrdinalIgnoreCase );
|
||||
|
||||
/// <summary>
|
||||
/// Call after loading from Json/Bson to replace # in field names to .
|
||||
/// </summary>
|
||||
public void PostLoad()
|
||||
{
|
||||
var fields = new Dictionary<string, SingleFieldMapping>( StringComparer.OrdinalIgnoreCase );
|
||||
var calculatedFields = new Dictionary<string, CalcFieldDef>( StringComparer.OrdinalIgnoreCase );
|
||||
var lookups = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
|
||||
|
||||
foreach ( var x in Fields )
|
||||
fields.Add( x.Key.Replace( "#", "." ), x.Value );
|
||||
foreach ( var x in CalculatedFields )
|
||||
calculatedFields.Add( x.Key.Replace( "#", "." ), x.Value );
|
||||
foreach ( var x in Lookups )
|
||||
lookups.Add( x.Key.Replace( "#", "." ), x.Value );
|
||||
|
||||
Fields = fields;
|
||||
CalculatedFields = calculatedFields;
|
||||
Lookups = lookups;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call before saving to Json/Bson to replace . in field names to #
|
||||
/// </summary>
|
||||
public void PreSave()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
CachedAt = new(now.Year, now.Month, now.Day, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var fields = new Dictionary<string, SingleFieldMapping>( StringComparer.OrdinalIgnoreCase );
|
||||
var calculatedFields = new Dictionary<string, CalcFieldDef>( StringComparer.OrdinalIgnoreCase );
|
||||
var lookups = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
|
||||
|
||||
foreach ( var x in Fields )
|
||||
fields.Add( x.Key.Replace( ".", "#" ), x.Value );
|
||||
foreach ( var x in CalculatedFields )
|
||||
calculatedFields.Add( x.Key.Replace( ".", "#" ), x.Value );
|
||||
foreach ( var x in Lookups )
|
||||
lookups.Add( x.Key.Replace( ".", "#" ), x.Value );
|
||||
|
||||
Fields = fields;
|
||||
CalculatedFields = calculatedFields;
|
||||
Lookups = lookups;
|
||||
}
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
|
||||
public FieldMappingData Clone()
|
||||
{
|
||||
var d = new FieldMappingData
|
||||
{
|
||||
CachedAt = CachedAt,
|
||||
UseMapping = UseMapping
|
||||
};
|
||||
|
||||
foreach ( var x in Fields )
|
||||
d.Fields.Add( x.Key, x.Value.Clone() );
|
||||
foreach ( var x in CalculatedFields )
|
||||
d.CalculatedFields.Add( x.Key, x.Value.Clone() );
|
||||
foreach ( var x in Lookups )
|
||||
d.Lookups.Add( x.Key, x.Value );
|
||||
|
||||
return d;
|
||||
}
|
||||
}
|
||||
677
Rms.Risk.Mango.Pivot.Core/Models/FilterExpressionTree.cs
Normal file
677
Rms.Risk.Mango.Pivot.Core/Models/FilterExpressionTree.cs
Normal file
@ -0,0 +1,677 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.IO;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public static class FilterExpressionTree
|
||||
{
|
||||
public const string IsoDatePrefix = "ISODate(\"";
|
||||
public const string IsoDateSuffix = "\")";
|
||||
|
||||
|
||||
public interface IFilterExpression
|
||||
{
|
||||
List<IFilterExpression> Children { get; }
|
||||
IFilterExpression Clone();
|
||||
}
|
||||
|
||||
public sealed class ExpressionGroup : IFilterExpression, ICloneable
|
||||
{
|
||||
public enum ConditionType { And, Or }
|
||||
|
||||
public ConditionType Condition { get; set; } = ConditionType.And;
|
||||
public List<IFilterExpression> Children { get; set; } = [];
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
|
||||
public IFilterExpression Clone()
|
||||
{
|
||||
var clone = new ExpressionGroup
|
||||
{
|
||||
Condition = Condition
|
||||
};
|
||||
|
||||
clone.Children.AddRange(Children.Select(x => x.Clone()));
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (IsEmpty)
|
||||
return string.Empty;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("( ");
|
||||
|
||||
for (var i = 0; i < Children.Count; i++)
|
||||
{
|
||||
sb.Append(Children[i]);
|
||||
if (i < Children.Count - 1)
|
||||
{
|
||||
sb.Append(Condition == ConditionType.And ? " AND " : " OR ");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(" )");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public bool IsEmpty => Children.Count == 0;
|
||||
|
||||
public string ToJson(Dictionary<string, Type> fieldTypes) => FilterExpressionTree.ToJson(this, fieldTypes);
|
||||
}
|
||||
|
||||
public enum FieldConditionType
|
||||
{
|
||||
[Description( "Contains") ] Contains,
|
||||
[Description( "Starts with") ] StartsWith,
|
||||
[Description( "Ends with") ] EndsWith,
|
||||
[Description( "==") ] EqualTo,
|
||||
[Description( "!=") ] NotEqualTo,
|
||||
[Description( ">") ] GreaterThan,
|
||||
[Description( "<") ] LessThan,
|
||||
[Description( ">=") ] GreaterThanOrEqualTo,
|
||||
[Description( "<=") ] LessThanOrEqualTo,
|
||||
[Description( "Is empty") ] IsEmpty,
|
||||
[Description( "Not is empty") ] NotIsEmpty,
|
||||
[Description( "Is null") ] IsNull,
|
||||
[Description( "Not is null") ] NotIsNull,
|
||||
[Description( "Matches") ] Matches,
|
||||
[Description( "Does not match") ] DoesNotMatch,
|
||||
[Description( "Does not contain") ] DoesNotContain,
|
||||
[Description( "Does not start with") ] DoesNotStartWith,
|
||||
[Description( "Does not end with") ] DoesNotEndWith,
|
||||
}
|
||||
|
||||
public sealed class FieldExpression : IFilterExpression, ICloneable
|
||||
{
|
||||
public List<IFilterExpression> Children => [];
|
||||
|
||||
public FieldConditionType Condition { get; set; } = FieldConditionType.EqualTo;
|
||||
|
||||
public string Field
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
field = value.Trim();
|
||||
}
|
||||
} = "";
|
||||
|
||||
public string Argument
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
field = value.Trim();
|
||||
}
|
||||
} = "";
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
|
||||
public IFilterExpression Clone()
|
||||
{
|
||||
var clone = new FieldExpression
|
||||
{
|
||||
Condition = Condition,
|
||||
Field = Field,
|
||||
Argument = Argument
|
||||
};
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var description = Condition.GetType()
|
||||
.GetField(Condition.ToString())
|
||||
?.GetCustomAttributes(typeof(DescriptionAttribute), false)
|
||||
.FirstOrDefault() as DescriptionAttribute;
|
||||
|
||||
return $"{Field} {description?.Description.ToLower() ?? Condition.ToString()} {Argument}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static ExpressionGroup ParseJson( string filterText )
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filterText))
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
BsonDocument? bson;
|
||||
try
|
||||
{
|
||||
bson = BsonDocument.Parse( filterText );
|
||||
|
||||
var e = bson.Elements.First();
|
||||
if ( e.Name != "$and" )
|
||||
bson = null;
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
// ignore
|
||||
bson = null;
|
||||
}
|
||||
|
||||
if ( bson == null )
|
||||
{
|
||||
try
|
||||
{
|
||||
bson = BsonDocument.Parse($"{{ \"$and\" : [ {filterText } ] }}");
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (ParseCondition( bson.Elements.First() ) is ExpressionGroup cond)
|
||||
return cond;
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
public static string ToJson(IFilterExpression? filter, IDictionary<string, Type> fieldTypes)
|
||||
{
|
||||
if (filter == null || (filter is ExpressionGroup grp && (grp.Children?.Count ?? 0) == 0))
|
||||
return "";
|
||||
|
||||
var bson = MakeJsonExpression(filter, fieldTypes);
|
||||
if ( bson == null )
|
||||
return "";
|
||||
|
||||
return bson.ElementCount > 0
|
||||
? bson.ToJson(new() { Indent = true, OutputMode = JsonOutputMode.RelaxedExtendedJson })
|
||||
: "";
|
||||
}
|
||||
|
||||
public static string ToSQL(ExpressionGroup parsedFilter, IDictionary<string, Type> fieldTypes)
|
||||
{
|
||||
if (parsedFilter == null || (parsedFilter.Children?.Count ?? 0) == 0)
|
||||
return "";
|
||||
|
||||
var sql = MakeSqlExpression(parsedFilter, fieldTypes, 0);
|
||||
return sql;
|
||||
}
|
||||
|
||||
private static IFilterExpression? ParseCondition(BsonElement element)
|
||||
{
|
||||
switch (element.Name)
|
||||
{
|
||||
case "$and":
|
||||
case "$or":
|
||||
{
|
||||
var cond = new ExpressionGroup
|
||||
{
|
||||
Condition = element.Name == "$and"
|
||||
? ExpressionGroup.ConditionType.And
|
||||
: ExpressionGroup.ConditionType.Or
|
||||
};
|
||||
|
||||
foreach (var e in element.Value.AsBsonArray.Select(x => x.AsBsonDocument.First()))
|
||||
{
|
||||
var expr = ParseCondition(e);
|
||||
if ( expr == null )
|
||||
continue;
|
||||
|
||||
cond.Children.Add( expr );
|
||||
}
|
||||
|
||||
return cond;
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
{
|
||||
var fieldName = element.Name;
|
||||
if ( !element.Value.IsBsonDocument )
|
||||
{
|
||||
var val = element.Value.ToString() ?? "";
|
||||
|
||||
if ( val.StartsWith( "/" ) && val.EndsWith( "/" ))
|
||||
return ParsePropertyExpression(fieldName, "$regex", val);
|
||||
return ParsePropertyExpression( fieldName, "$eq", val );
|
||||
}
|
||||
|
||||
var e = element.Value.AsBsonDocument.Elements.First();
|
||||
var op = e.Name;
|
||||
var arg = e.Value.ToString() ?? "";
|
||||
return ParsePropertyExpression( fieldName, op, arg );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IFilterExpression? ParsePropertyExpression( string property, string op, string arg )
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case "$ne":
|
||||
return CreateBinaryExpression(property, arg, FieldConditionType.NotEqualTo);
|
||||
case "$eq":
|
||||
return CreateBinaryExpression(property, arg, FieldConditionType.EqualTo);
|
||||
case "$lte":
|
||||
return CreateBinaryExpression(property, arg, FieldConditionType.LessThanOrEqualTo);
|
||||
case "$lt":
|
||||
return CreateBinaryExpression(property, arg, FieldConditionType.LessThan);
|
||||
case "$gte":
|
||||
return CreateBinaryExpression(property, arg, FieldConditionType.GreaterThanOrEqualTo);
|
||||
case "$gt":
|
||||
return CreateBinaryExpression(property, arg, FieldConditionType.GreaterThan);
|
||||
case "$regex":
|
||||
{
|
||||
arg = arg.Trim( '/' );
|
||||
var newCond = RegexToCondition( arg, out var newArg );
|
||||
|
||||
return CreateBinaryExpression( property, newArg ?? arg, newCond );
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static IFilterExpression CreateBinaryExpression(string property, string arg, FieldConditionType c)
|
||||
=> new FieldExpression {Field = property, Condition = c, Argument = arg};
|
||||
|
||||
|
||||
private static string Shield(string s) =>
|
||||
s
|
||||
.Replace("\\", "\\\\") // first
|
||||
.Replace(".", "\\.")
|
||||
.Replace("(", "\\(")
|
||||
.Replace(")", "\\)")
|
||||
.Replace("*", "\\*")
|
||||
.Replace("?", "\\?")
|
||||
.Replace("[", "\\]")
|
||||
.Replace("]", "\\]");
|
||||
|
||||
private static string Unshield(string s) =>
|
||||
s
|
||||
.Replace("\\.", ".")
|
||||
.Replace("\\(", "(")
|
||||
.Replace("\\)", ")")
|
||||
.Replace("\\*", "*")
|
||||
.Replace("\\?", "?")
|
||||
.Replace("\\]", "[")
|
||||
.Replace("\\]", "]")
|
||||
.Replace("\\\\", "\\"); // last
|
||||
|
||||
private static FieldConditionType RegexToCondition( string regex, out string? arg )
|
||||
{
|
||||
arg = null;
|
||||
switch ( regex )
|
||||
{
|
||||
case "^$":
|
||||
return FieldConditionType.IsEmpty;
|
||||
case "^.+$":
|
||||
return FieldConditionType.NotIsEmpty;
|
||||
default:
|
||||
if ( regex.StartsWith( "^.*" ) && regex.EndsWith(".*$"))
|
||||
{
|
||||
arg = Unshield(regex.Substring( 3, regex.Length - 3 - 3 ));
|
||||
return FieldConditionType.Contains;
|
||||
}
|
||||
if (regex.StartsWith("^.*") && regex.EndsWith("$"))
|
||||
{
|
||||
arg = Unshield(regex.Substring(3, regex.Length - 3 - 1 ));
|
||||
return FieldConditionType.EndsWith;
|
||||
}
|
||||
if (regex.StartsWith("^") && regex.EndsWith(".*$"))
|
||||
{
|
||||
arg = Unshield(regex.Substring(1, regex.Length - 1- 3));
|
||||
return FieldConditionType.StartsWith;
|
||||
}
|
||||
if (regex.StartsWith("^(?!") && regex.EndsWith(")$"))
|
||||
{
|
||||
arg = Unshield(regex.Substring(1, regex.Length - 4 - 2));
|
||||
return FieldConditionType.DoesNotMatch;
|
||||
}
|
||||
if (regex.StartsWith("^.*(?!") && regex.EndsWith(").*$"))
|
||||
{
|
||||
arg = Unshield(regex.Substring(1, regex.Length - 6 - 4));
|
||||
return FieldConditionType.DoesNotContain;
|
||||
}
|
||||
if (regex.StartsWith("^(?!") && regex.EndsWith(").*$"))
|
||||
{
|
||||
arg = Unshield(regex.Substring(1, regex.Length - 4 - 4));
|
||||
return FieldConditionType.DoesNotStartWith;
|
||||
}
|
||||
if (regex.StartsWith("^.*(?!") && regex.EndsWith(")$"))
|
||||
{
|
||||
arg = Unshield(regex.Substring(1, regex.Length - 6 - 2));
|
||||
return FieldConditionType.DoesNotEndWith;
|
||||
}
|
||||
return FieldConditionType.Matches;
|
||||
}
|
||||
}
|
||||
|
||||
private static BsonDocument? MakeJsonExpression(IFilterExpression cond, IDictionary<string, Type> fieldTypes)
|
||||
{
|
||||
if (cond == null)
|
||||
throw new InvalidExpressionException("Expected FieldExpression but got NULL" );
|
||||
|
||||
switch ( cond )
|
||||
{
|
||||
case ExpressionGroup when cond.Children.Count == 0:
|
||||
// empty group, return null
|
||||
return null;
|
||||
case ExpressionGroup when cond.Children is [FieldExpression { Condition: FieldConditionType.EqualTo }]:
|
||||
// skip grouping if only one child
|
||||
{
|
||||
var fieldExpr = (FieldExpression)cond.Children[0];
|
||||
return [new BsonElement(fieldExpr.Field, ConvertValue(fieldExpr.Field, fieldExpr.Argument))];
|
||||
}
|
||||
case ExpressionGroup group:
|
||||
var items = cond.Children.Select( cond1 => MakeJsonExpression(cond1, fieldTypes) ).Where( x => x != null ).ToList();
|
||||
if ( items.Count == 0 )
|
||||
return null;
|
||||
|
||||
var elem = new BsonElement(group.Condition == ExpressionGroup.ConditionType.And ? "$and" : "$or", new BsonArray(items));
|
||||
return [elem];
|
||||
}
|
||||
|
||||
if ( cond is not FieldExpression propExpr )
|
||||
throw new InvalidExpressionException($"Expected FieldExpression but got {cond} ({cond.GetType()})" );
|
||||
|
||||
var prop = propExpr.Field;
|
||||
var op = propExpr.Condition;
|
||||
var val = propExpr.Argument;
|
||||
|
||||
var arg = ConvertValue(prop, val);
|
||||
|
||||
var o = ConvertCondition( op, arg.ToString() ?? "", out var regex );
|
||||
var d = new BsonDocument {new( o, regex ?? arg )};
|
||||
|
||||
return [new BsonElement(prop, d)];
|
||||
|
||||
BsonValue ConvertValue(string name, string value)
|
||||
{
|
||||
if (!fieldTypes.TryGetValue(name, out var propType))
|
||||
propType = typeof(string);
|
||||
|
||||
BsonValue bsonValue;
|
||||
if (propType == typeof(double))
|
||||
bsonValue = new BsonDouble(Convert.ToDouble(value));
|
||||
else if (propType == typeof(int))
|
||||
bsonValue = new BsonInt32(Convert.ToInt32(value));
|
||||
else if (propType == typeof(decimal))
|
||||
bsonValue = new BsonDouble(Convert.ToDouble(value));
|
||||
else if (propType == typeof(long))
|
||||
bsonValue = new BsonInt64(Convert.ToInt64(value));
|
||||
else bsonValue = propType == typeof(DateTime) || value.StartsWith(IsoDatePrefix)
|
||||
? new BsonDateTime(ConvertToDateTime(value))
|
||||
: new BsonString(value)
|
||||
;
|
||||
return bsonValue;
|
||||
}
|
||||
}
|
||||
|
||||
private const DateTimeStyles DateTimeStyle = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal;
|
||||
|
||||
private static DateTime ConvertToDateTime(string val)
|
||||
{
|
||||
|
||||
if (val.StartsWith(IsoDatePrefix) && val.EndsWith(IsoDateSuffix))
|
||||
{
|
||||
var inner = val.Substring(IsoDatePrefix.Length, val.Length - IsoDatePrefix.Length - IsoDateSuffix.Length);
|
||||
return ConvertExact(inner);
|
||||
}
|
||||
|
||||
return ConvertExact(val);
|
||||
|
||||
DateTime ConvertExact(string value)
|
||||
{
|
||||
var formats = new[]
|
||||
{
|
||||
"yyyy-MM-ddTHH:mm:ssZ",
|
||||
"yyyy-MM-ddTHH:mm:ss.fffZ",
|
||||
"yyyy-MM-ddTHH:mm:ssK",
|
||||
"yyyy-MM-ddTHH:mm:ss.fffK",
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
"yyyy-MM-dd HH:mm:ss.fff",
|
||||
"yyyy-MM-dd",
|
||||
"yyyy/MM/ddTHH:mm:ssZ",
|
||||
"yyyy/MM/ddTHH:mm:ss.fffZ",
|
||||
"yyyy/MM/ddTHH:mm:ssK",
|
||||
"yyyy/MM/ddTHH:mm:ss.fffK",
|
||||
"yyyy/MM/dd HH:mm:ss",
|
||||
"yyyy/MM/dd HH:mm:ss.fff",
|
||||
"yyyy/MM/dd",
|
||||
// really don't recommend these
|
||||
"dd/MM/yyyy",
|
||||
"dd/MM/yyyy HH:mm:ss",
|
||||
"dd/MM/yyyy HH:mm:ss.fff",
|
||||
"dd-MM-yyyy"
|
||||
};
|
||||
|
||||
foreach ( var format in formats )
|
||||
{
|
||||
if (DateTime.TryParseExact(value, format, null, DateTimeStyle, out var d))
|
||||
return DateTime.SpecifyKind(d, DateTimeKind.Utc);
|
||||
}
|
||||
throw new ApplicationException("Invalid date/time: " + value);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertCondition(FieldConditionType op, string arg, out string? regex)
|
||||
{
|
||||
regex = null;
|
||||
switch (op)
|
||||
{
|
||||
case FieldConditionType.EqualTo:
|
||||
return "$eq";
|
||||
case FieldConditionType.NotEqualTo:
|
||||
return "$ne";
|
||||
case FieldConditionType.LessThanOrEqualTo:
|
||||
return "$lte";
|
||||
case FieldConditionType.LessThan:
|
||||
return "$lt";
|
||||
case FieldConditionType.GreaterThanOrEqualTo:
|
||||
return "$gte";
|
||||
case FieldConditionType.GreaterThan:
|
||||
return "$gt";
|
||||
case FieldConditionType.Contains:
|
||||
regex = $"^.*{Shield(arg)}.*$";
|
||||
return "$regex";
|
||||
case FieldConditionType.StartsWith:
|
||||
regex = $"^{Shield(arg)}.*$";
|
||||
return "$regex";
|
||||
case FieldConditionType.EndsWith:
|
||||
regex = $"^.*{Shield(arg)}$";
|
||||
return "$regex";
|
||||
case FieldConditionType.IsEmpty:
|
||||
regex = "^$";
|
||||
return "$regex";
|
||||
case FieldConditionType.NotIsEmpty:
|
||||
regex = "^.+$";
|
||||
return "$regex";
|
||||
case FieldConditionType.IsNull:
|
||||
regex = "^$";
|
||||
return "$regex";
|
||||
case FieldConditionType.NotIsNull:
|
||||
regex = "^.+$";
|
||||
return "$regex";
|
||||
case FieldConditionType.Matches:
|
||||
return "$regex";
|
||||
case FieldConditionType.DoesNotMatch:
|
||||
regex = $"^(?!{arg})$";
|
||||
return "$regex";
|
||||
case FieldConditionType.DoesNotContain:
|
||||
// https://stackoverflow.com/a/406408
|
||||
regex = $"^((?!{Shield(arg)}).)*$";
|
||||
return "$regex";
|
||||
case FieldConditionType.DoesNotStartWith:
|
||||
regex = $"^(?!{Shield(arg)}).*$";
|
||||
return "$regex";
|
||||
case FieldConditionType.DoesNotEndWith:
|
||||
regex = $"^.*(?!{Shield(arg)})$";
|
||||
return "$regex";
|
||||
default:
|
||||
throw new ApplicationException($"Unsupported operation {op}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// https://www.codeproject.com/Tips/483763/Equivalent-function-of-mysql-real-escape-string-in
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
private static string ShieldSql(string str) =>
|
||||
Regex.Replace(str, @"[\x00'""\b\n\r\t\cZ\\%_]",
|
||||
delegate(Match match)
|
||||
{
|
||||
var v = match.Value;
|
||||
switch (v)
|
||||
{
|
||||
case "\x00": // ASCII NUL (0x00) character
|
||||
return "\\0";
|
||||
case "\b": // BACKSPACE character
|
||||
return "\\b";
|
||||
case "\n": // NEWLINE (linefeed) character
|
||||
return "\\n";
|
||||
case "\r": // CARRIAGE RETURN character
|
||||
return "\\r";
|
||||
case "\t": // TAB
|
||||
return "\\t";
|
||||
case "\u001A": // Ctrl-Z
|
||||
return "\\Z";
|
||||
default:
|
||||
return "\\" + v;
|
||||
}
|
||||
});
|
||||
|
||||
private static string MakeSqlExpression(IFilterExpression filter, IDictionary<string, Type> fieldTypes, int level)
|
||||
{
|
||||
var prefix = new string(' ', level);
|
||||
if (filter is ExpressionGroup group)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
if ( group.Condition == ExpressionGroup.ConditionType.And )
|
||||
{
|
||||
sb.AppendJoin(
|
||||
$"\n\t{prefix}AND ",
|
||||
group.Children.Select(x => MakeSqlExpression(x, fieldTypes, level+1)));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"\n\t{prefix}(");
|
||||
sb.AppendJoin(
|
||||
$"\n\t{prefix}OR ",
|
||||
group.Children.Select(x => MakeSqlExpression(x, fieldTypes, level+1)));
|
||||
sb.AppendLine($"\n\t{prefix})");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
var expr = (FieldExpression) filter;
|
||||
var s = $"\t{prefix}{expr.Field} {GetSqlCondition(expr, fieldTypes)}";
|
||||
return s;
|
||||
}
|
||||
|
||||
private static string GetSqlArg(FieldExpression expr, IDictionary<string, Type> fieldTypes)
|
||||
{
|
||||
if (expr.Argument == null)
|
||||
return "''";
|
||||
|
||||
if (!fieldTypes.TryGetValue(expr.Field, out var propType))
|
||||
propType = typeof(string);
|
||||
|
||||
if (propType == typeof(string))
|
||||
return $"'{ShieldSql(expr.Argument)}'";
|
||||
if (propType == typeof(double))
|
||||
return string.IsNullOrWhiteSpace(expr.Argument) ? "0" : Convert.ToDouble(expr.Argument).ToString(CultureInfo.InvariantCulture);
|
||||
if (propType == typeof(int))
|
||||
return string.IsNullOrWhiteSpace(expr.Argument) ? "0" : Convert.ToInt32(expr.Argument).ToString(CultureInfo.InvariantCulture);
|
||||
if (propType == typeof(decimal))
|
||||
return string.IsNullOrWhiteSpace(expr.Argument) ? "0" : Convert.ToDouble(expr.Argument).ToString(CultureInfo.InvariantCulture);
|
||||
if (propType == typeof(long))
|
||||
return string.IsNullOrWhiteSpace(expr.Argument) ? "0" : Convert.ToInt64(expr.Argument).ToString(CultureInfo.InvariantCulture);
|
||||
if (propType == typeof(DateTime))
|
||||
return string.IsNullOrWhiteSpace(expr.Argument)
|
||||
? "0"
|
||||
: $"timestamp( '{ToDateTime(expr):yyyy-MM-dd HH:mm:ss}' )"; // should work for Oracle and ClickHouse
|
||||
if (propType == typeof(DateOnly))
|
||||
return string.IsNullOrWhiteSpace(expr.Argument)
|
||||
? "0"
|
||||
: $"timestamp( '{ToDateOnly(expr):yyyy-MM-dd}' )"; // should work for Oracle and ClickHouse
|
||||
return ShieldSql(expr.Argument);
|
||||
}
|
||||
|
||||
private static DateTime ToDateTime(FieldExpression expr)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DateTime.Parse(expr.Argument, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return DateTime.ParseExact(expr.Argument, "dd-MM-yyyy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
|
||||
}
|
||||
}
|
||||
|
||||
private static DateOnly ToDateOnly(FieldExpression expr)
|
||||
=> DateOnly.Parse(expr.Argument, CultureInfo.InvariantCulture);
|
||||
|
||||
private static string GetSqlCondition(FieldExpression expr, IDictionary<string, Type> fieldTypes)
|
||||
{
|
||||
var arg = GetSqlArg(expr, fieldTypes);
|
||||
return expr.Condition switch
|
||||
{
|
||||
FieldConditionType.EqualTo => $"= {arg}",
|
||||
FieldConditionType.NotEqualTo => $"!= {arg}",
|
||||
FieldConditionType.LessThanOrEqualTo => $"<= {arg}",
|
||||
FieldConditionType.LessThan => $"< {arg}",
|
||||
FieldConditionType.GreaterThanOrEqualTo => $">= {arg}",
|
||||
FieldConditionType.GreaterThan => $"> {arg}",
|
||||
FieldConditionType.Contains => $"LIKE '%{ShieldSql(expr.Argument)}%'",
|
||||
FieldConditionType.StartsWith => $"LIKE '{ShieldSql(expr.Argument)}%'",
|
||||
FieldConditionType.EndsWith => $"LIKE '%{ShieldSql(expr.Argument)}'",
|
||||
FieldConditionType.IsEmpty => "= ''",
|
||||
FieldConditionType.NotIsEmpty => "!= ''",
|
||||
FieldConditionType.IsNull => "IS NULL",
|
||||
FieldConditionType.NotIsNull => "IS NOT NULL",
|
||||
FieldConditionType.Matches => $"LIKE '{ShieldSql(expr.Argument)}'",
|
||||
FieldConditionType.DoesNotMatch => $"NOT LIKE '{ShieldSql(expr.Argument)}'",
|
||||
FieldConditionType.DoesNotContain => $"NOT LIKE '%{ShieldSql(expr.Argument)}%'",
|
||||
FieldConditionType.DoesNotStartWith => $"NOT LIKE '{ShieldSql(expr.Argument)}%'",
|
||||
FieldConditionType.DoesNotEndWith => $"NOT LIKE '%{ShieldSql(expr.Argument)}'",
|
||||
_ => throw new ApplicationException($"Unsupported operation {expr.Condition}")
|
||||
};
|
||||
}
|
||||
}
|
||||
92
Rms.Risk.Mango.Pivot.Core/Models/IndexInfoModel.cs
Normal file
92
Rms.Risk.Mango.Pivot.Core/Models/IndexInfoModel.cs
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class IndexesInfoModel
|
||||
{
|
||||
public List<IndexInfoModel> Indexes { get; set; } = new();
|
||||
|
||||
public static IndexesInfoModel FromBson(BsonDocument bsonDocument)
|
||||
{
|
||||
var model = new IndexesInfoModel();
|
||||
var indexesArray = bsonDocument["cursor"]["firstBatch"].AsBsonArray;
|
||||
|
||||
foreach (var indexElement in indexesArray)
|
||||
{
|
||||
var indexDoc = indexElement.AsBsonDocument;
|
||||
var index = new IndexInfoModel
|
||||
{
|
||||
Version = indexDoc.Contains("v") ? indexDoc["v"].AsInt32 : 0,
|
||||
Key = indexDoc.Contains("key") && indexDoc["key"].IsBsonDocument
|
||||
? indexDoc["key"].AsBsonDocument.ToDictionary(k => k.Name, v => v.Value.ToString() ?? "")
|
||||
: new Dictionary<string, string>(),
|
||||
Name = indexDoc.Contains("name") ? indexDoc["name"].AsString : string.Empty,
|
||||
ExpireAfterSeconds = indexDoc.Contains("expireAfterSeconds") ? indexDoc["expireAfterSeconds"].ToNullableInt32() : null
|
||||
};
|
||||
model.Indexes.Add(index);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public void CopyFrom(IndexesInfoModel source)
|
||||
{
|
||||
Indexes = source.Indexes.Select(index => index.Clone()).ToList();
|
||||
}
|
||||
|
||||
public IndexesInfoModel Clone()
|
||||
{
|
||||
var clone = new IndexesInfoModel();
|
||||
clone.CopyFrom(this);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
public class IndexInfoModel
|
||||
{
|
||||
public int Version { get; set; }
|
||||
public Dictionary<string, string> Key { get; set; } = new();
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public int? ExpireAfterSeconds { get; set; }
|
||||
|
||||
public void CopyFrom(IndexInfoModel source)
|
||||
{
|
||||
Version = source.Version;
|
||||
Key = new Dictionary<string, string>(source.Key);
|
||||
Name = source.Name;
|
||||
ExpireAfterSeconds = source.ExpireAfterSeconds;
|
||||
}
|
||||
|
||||
public IndexInfoModel Clone()
|
||||
{
|
||||
var clone = new IndexInfoModel();
|
||||
clone.CopyFrom(this);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BsonExtensions
|
||||
{
|
||||
public static int? ToNullableInt32(this BsonValue value)
|
||||
{
|
||||
return value.IsBsonNull ? (int?)null : value.AsInt32;
|
||||
}
|
||||
}
|
||||
47
Rms.Risk.Mango.Pivot.Core/Models/LogsModel.cs
Normal file
47
Rms.Risk.Mango.Pivot.Core/Models/LogsModel.cs
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class LogRecordModel
|
||||
{
|
||||
|
||||
public DateTime Time { get; set; }
|
||||
public string Severity { get; set; } = string.Empty;
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public long Id { get; set; }
|
||||
public string Svc { get; set; } = string.Empty;
|
||||
public string Ctx { get; set; } = string.Empty;
|
||||
public string Msg { get; set; } = string.Empty;
|
||||
public BsonDocument? Attr { get; set; }
|
||||
|
||||
public static LogRecordModel FromBson(BsonDocument doc) =>
|
||||
new ()
|
||||
{
|
||||
Time = doc.Contains("t") ? doc["t"].ToUniversalTime() : default,
|
||||
Severity = doc.Contains("s") ? doc["s"].AsString : string.Empty,
|
||||
Category = doc.Contains("c") ? doc["c"].AsString : string.Empty,
|
||||
Id = doc.Contains("id") ? doc["id"].ToInt64() : 0L,
|
||||
Svc = doc.Contains("svc") ? doc["svc"].AsString : string.Empty,
|
||||
Ctx = doc.Contains("ctx") ? doc["ctx"].AsString : string.Empty,
|
||||
Msg = doc.Contains("msg") ? doc["msg"].AsString : string.Empty,
|
||||
Attr = doc.Contains("attr") ? doc["attr"].AsBsonDocument : null
|
||||
};
|
||||
}
|
||||
32
Rms.Risk.Mango.Pivot.Core/Models/MongoDatabaseInfo.cs
Normal file
32
Rms.Risk.Mango.Pivot.Core/Models/MongoDatabaseInfo.cs
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class MongoDatabaseInfo
|
||||
{
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||
|
||||
[JsonPropertyName( "sizeOnDisk")]
|
||||
public decimal SizeOnDisk { get; set; }
|
||||
|
||||
[JsonPropertyName( "empty")]
|
||||
public bool Empty { get; set; }
|
||||
}
|
||||
161
Rms.Risk.Mango.Pivot.Core/Models/MongoDbCachingHelper.cs
Normal file
161
Rms.Risk.Mango.Pivot.Core/Models/MongoDbCachingHelper.cs
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public static class MongoDbCachingHelper
|
||||
{
|
||||
public static async Task<string[]> LoadCachedCobDatesAsync(this MongoDbDataSource source, string collectionName, CancellationToken token = default)
|
||||
{
|
||||
var coll = source.GetCollectionWithRetries<BsonDocument>(collectionName+"-Meta");
|
||||
var cursor = await coll.FindAsync("{ _id : \"CachedCobDates\"}", cancellationToken: token);
|
||||
|
||||
while ( await cursor.MoveNextAsync(token) )
|
||||
{
|
||||
var batch = cursor.Current;
|
||||
foreach ( var doc in batch )
|
||||
{
|
||||
if ( doc.IsBsonNull )
|
||||
return [];
|
||||
|
||||
var res = doc.ToDictionary();
|
||||
|
||||
return res["CobDates"] is not object[] cobs
|
||||
? []
|
||||
: cobs.Select( x => x as string )
|
||||
.Where( x => !string.IsNullOrWhiteSpace( x ) )
|
||||
.OfType<string>()
|
||||
.ToArray()
|
||||
;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private class CachedDepartmentsDoc
|
||||
{
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
||||
[BsonId] public string Id { get; set; } = "";
|
||||
public string[] Departments { get; set; } = [];
|
||||
public DateTime CachedOnUtc { get; set; }
|
||||
}
|
||||
|
||||
private const string CachedDepartmentsDocName = "CachedDepartments";
|
||||
|
||||
public static async Task<Tuple<bool,string[]>> LoadCachedDepartmentsAsync(this MongoDbDataSource source, string collectionName, CancellationToken token = default)
|
||||
{
|
||||
var coll = source.GetCollectionWithRetries<CachedDepartmentsDoc>(collectionName+"-Meta");
|
||||
var cursor = await coll.FindAsync($"{{ _id : \"{CachedDepartmentsDocName}\"}}", cancellationToken: token);
|
||||
|
||||
while ( await cursor.MoveNextAsync(token) )
|
||||
{
|
||||
var batch = cursor.Current;
|
||||
foreach ( var doc in batch )
|
||||
{
|
||||
var expireAt = doc.CachedOnUtc + TimeSpan.FromHours(1);
|
||||
var isStillValid = expireAt > DateTime.UtcNow;
|
||||
|
||||
return Tuple.Create(
|
||||
isStillValid,
|
||||
doc.Departments.Where( x => !string.IsNullOrWhiteSpace( x ) ).ToArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
return Tuple.Create(false, Array.Empty<string>());
|
||||
}
|
||||
|
||||
public static async Task CacheDepartments(this MongoDbDataSource source, string collectionName, string [] departments, CancellationToken token = default)
|
||||
{
|
||||
if ( departments.Length == 0 )
|
||||
return;
|
||||
|
||||
var doc = new CachedDepartmentsDoc()
|
||||
{
|
||||
Id = CachedDepartmentsDocName,
|
||||
CachedOnUtc = DateTime.UtcNow,
|
||||
Departments = departments
|
||||
};
|
||||
|
||||
var coll = source.GetCollectionWithRetries<CachedDepartmentsDoc>(collectionName + "-Meta");
|
||||
await coll.ReplaceOneAsync( $"{{ _id : \"{CachedDepartmentsDocName}\"}}", doc, new ReplaceOptions {IsUpsert = true}, token);
|
||||
}
|
||||
|
||||
private class CachedDesksDoc
|
||||
{
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
||||
[BsonId] public string Id { get; set; } = "";
|
||||
public (string, string)[] DeskAndDepartment { get; set; } = [];
|
||||
public DateTime CachedOnUtc { get; set; }
|
||||
}
|
||||
|
||||
private const string CachedDesksDocName = "CachedDesks";
|
||||
|
||||
public static async Task<Tuple<bool,(string, string)[]>> LoadCachedDesksAsync(this MongoDbDataSource source, string collectionName, CancellationToken token = default)
|
||||
{
|
||||
var coll = source.GetCollectionWithRetries<CachedDesksDoc>(collectionName+"-Meta");
|
||||
var cursor = await coll.FindAsync($"{{ _id : \"{CachedDesksDocName}\"}}", cancellationToken: token);
|
||||
|
||||
while ( await cursor.MoveNextAsync(token) )
|
||||
{
|
||||
var batch = cursor.Current;
|
||||
foreach ( var doc in batch )
|
||||
{
|
||||
var expireAt = doc.CachedOnUtc + TimeSpan.FromHours(24);
|
||||
var isStillValid = expireAt > DateTime.UtcNow;
|
||||
|
||||
return Tuple.Create(
|
||||
isStillValid,
|
||||
doc.DeskAndDepartment.Where( x => !string.IsNullOrWhiteSpace( x.Item1 ) &&
|
||||
!string.IsNullOrWhiteSpace( x.Item2 )).ToArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
return Tuple.Create(false, Array.Empty<(string, string)>());
|
||||
}
|
||||
|
||||
public static async Task CacheDesks(this MongoDbDataSource source, string collectionName, (string, string) [] desks, CancellationToken token = default)
|
||||
{
|
||||
if ( desks.Length == 0 )
|
||||
return;
|
||||
|
||||
var doc = new CachedDesksDoc()
|
||||
{
|
||||
Id = CachedDesksDocName,
|
||||
CachedOnUtc = DateTime.UtcNow,
|
||||
DeskAndDepartment = desks
|
||||
};
|
||||
|
||||
var coll = source.GetCollectionWithRetries<CachedDesksDoc>(collectionName + "-Meta");
|
||||
await coll.ReplaceOneAsync( $"{{ _id : \"{CachedDesksDocName}\"}}", doc, new ReplaceOptions {IsUpsert = true}, token);
|
||||
}
|
||||
|
||||
public static async Task CacheCobDates(this MongoDbDataSource source, string collectionName, string [] cobs, CancellationToken token = default)
|
||||
{
|
||||
if ( cobs.Length == 0 )
|
||||
return;
|
||||
|
||||
var doc = new BsonDocument(new Dictionary<string, string[]> { ["CobDates"] = cobs});
|
||||
var coll = source.GetCollectionWithRetries<BsonDocument>(collectionName + "-Meta");
|
||||
await coll.ReplaceOneAsync( "{_id : \"CachedCobDates\"}", doc, new ReplaceOptions {IsUpsert = true}, token);
|
||||
}
|
||||
|
||||
}
|
||||
1513
Rms.Risk.Mango.Pivot.Core/Models/MongoDbDataSource.cs
Normal file
1513
Rms.Risk.Mango.Pivot.Core/Models/MongoDbDataSource.cs
Normal file
File diff suppressed because it is too large
Load Diff
31
Rms.Risk.Mango.Pivot.Core/Models/MongoShardInfo.cs
Normal file
31
Rms.Risk.Mango.Pivot.Core/Models/MongoShardInfo.cs
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class MongoShardInfo
|
||||
{
|
||||
[JsonPropertyName("_id")] public string Id { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("host")] public string Host { get; set; } = "";
|
||||
|
||||
[JsonPropertyName( "state")]
|
||||
public int State { get; set; }
|
||||
}
|
||||
92
Rms.Risk.Mango.Pivot.Core/Models/Navigation.cs
Normal file
92
Rms.Risk.Mango.Pivot.Core/Models/Navigation.cs
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class Navigation<T>(Action<PivotDefinition, T> apply) where T : class
|
||||
{
|
||||
|
||||
private class NavigationRecord
|
||||
{
|
||||
public PivotDefinition? Def { get; init; }
|
||||
public T? Data { get; init; }
|
||||
|
||||
public override string ToString() => $"{Def?.Name} Hash={Def?.GetHashCode():X}";
|
||||
}
|
||||
|
||||
private readonly Action<PivotDefinition,T> _apply = apply;
|
||||
private readonly Stack<NavigationRecord> _backStack = new();
|
||||
private readonly Stack<NavigationRecord> _forwardStack = new();
|
||||
private NavigationRecord? _current;
|
||||
|
||||
public bool CanGoBackward => _backStack.Count > 0;
|
||||
public bool CanGoForward => _forwardStack.Count > 0;
|
||||
|
||||
public void Add( PivotDefinition defToAdd, T data )
|
||||
{
|
||||
var rec = new NavigationRecord
|
||||
{
|
||||
Def = defToAdd,
|
||||
Data = data
|
||||
};
|
||||
if ( _current != null )
|
||||
_backStack.Push( _current );
|
||||
_current = rec;
|
||||
_forwardStack.Clear();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_current = null;
|
||||
_backStack.Clear();
|
||||
_forwardStack.Clear();
|
||||
}
|
||||
|
||||
public bool Back()
|
||||
{
|
||||
if (_backStack.Count == 0)
|
||||
return false;
|
||||
|
||||
var rec = _backStack.Pop();
|
||||
if ( _current != null )
|
||||
_forwardStack.Push( _current );
|
||||
Apply( rec );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Forward()
|
||||
{
|
||||
if (_forwardStack.Count == 0)
|
||||
return false;
|
||||
|
||||
var rec = _forwardStack.Pop();
|
||||
if ( _current != null )
|
||||
_backStack.Push( rec );
|
||||
|
||||
Apply( rec);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Apply( NavigationRecord rec )
|
||||
{
|
||||
_current = rec;
|
||||
_apply(rec.Def!, rec.Data!);
|
||||
}
|
||||
}
|
||||
179
Rms.Risk.Mango.Pivot.Core/Models/NotifyPropertyChangedBase.cs
Normal file
179
Rms.Risk.Mango.Pivot.Core/Models/NotifyPropertyChangedBase.cs
Normal file
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.ComponentModel;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public interface IMutable
|
||||
{
|
||||
bool IsBusy { get; }
|
||||
void Mute();
|
||||
void Unmute();
|
||||
}
|
||||
|
||||
public class NotifyPropertyChangedBase : INotifyPropertyChanged, IMutable
|
||||
{
|
||||
private static Action<Action> _invokeWrapper = action => action?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// For WPF applications call:
|
||||
/// NotifyPropertyChangedBase.SetInvokeWrapper( Application.Current.Dispatcher.Invoke );
|
||||
/// </summary>
|
||||
/// <param name="invokeWrapper"></param>
|
||||
public static void SetInvokeWrapper(Action<Action> invokeWrapper)
|
||||
=> _invokeWrapper = invokeWrapper;
|
||||
|
||||
#region INotifyPropertyChanged Members
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name of a property dynamically using an expression tree without requiring an instance
|
||||
/// usage: GetPropertyName@gt;MyClass@lt;( (MyClass c) => c.MyProperty) returns "MyProperty"
|
||||
/// </summary>
|
||||
public static string GetPropertyName<TModel, TProperty>(Expression<Func<TModel, TProperty>> property)
|
||||
{
|
||||
var lambda = (LambdaExpression)property;
|
||||
MemberExpression memberExpression;
|
||||
|
||||
if (lambda.Body is UnaryExpression body)
|
||||
{
|
||||
var unaryExpression = body;
|
||||
memberExpression = (MemberExpression)unaryExpression.Operand;
|
||||
}
|
||||
else
|
||||
{
|
||||
memberExpression = (MemberExpression)lambda.Body;
|
||||
}
|
||||
|
||||
return memberExpression.Member.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name of a property dynamically using an expression tree
|
||||
/// usage: GetPropertyName( () => someinstance.MyProperty) returns "MyProperty"
|
||||
/// </summary>
|
||||
public static string GetPropertyName<T>(Expression<Func<T>> property)
|
||||
{
|
||||
var lambda = (LambdaExpression)property;
|
||||
MemberExpression memberExpression;
|
||||
|
||||
if (lambda.Body is UnaryExpression body)
|
||||
{
|
||||
var unaryExpression = body;
|
||||
memberExpression = (MemberExpression)unaryExpression.Operand;
|
||||
}
|
||||
else
|
||||
{
|
||||
memberExpression = (MemberExpression)lambda.Body;
|
||||
}
|
||||
|
||||
return memberExpression.Member.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overriden by subclasses, which may way want to to enable/diable the notification event.
|
||||
/// Called before every event notification
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual bool AllowPropertyChanged() => true;
|
||||
|
||||
/// <summary>
|
||||
/// Fires the property changed event
|
||||
/// Dynamically generates the property name using an expression tree
|
||||
/// usage: OnPropertyChanged( (MyClass m) => m.MyProperty)
|
||||
/// </summary>
|
||||
protected void OnPropertyChanged<TModel, TProperty>(Expression<Func<TModel, TProperty>> property)
|
||||
{
|
||||
var e = new PropertyChangedEventArgs(GetPropertyName(property));
|
||||
OnPropertyChanged(e);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Fires the property changed event
|
||||
/// Dynamically generates the property name using an expression tree
|
||||
/// usage: OnPropertyChanged( () => MyProperty)
|
||||
/// </summary>
|
||||
protected void OnPropertyChanged<T>(Expression<Func<T>> property)
|
||||
{
|
||||
var e = new PropertyChangedEventArgs(GetPropertyName(property));
|
||||
OnPropertyChanged(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fire the notify property changed event with a static property name
|
||||
/// No not supply propName as it'll be automatically filled in
|
||||
/// </summary>
|
||||
protected void OnPropertyChanged([CallerMemberName] string propName = "")
|
||||
{
|
||||
var e = new PropertyChangedEventArgs(propName);
|
||||
OnPropertyChanged(e);
|
||||
}
|
||||
|
||||
private void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
// if (IsBusy)
|
||||
// return;
|
||||
|
||||
var pc = PropertyChanged;
|
||||
if (pc != null && AllowPropertyChanged())
|
||||
{
|
||||
_invokeWrapper( () => pc(this, e) );
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsBusy
|
||||
{
|
||||
get => _muteCount > 0;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
Mute();
|
||||
else
|
||||
Unmute();
|
||||
}
|
||||
}
|
||||
|
||||
private int _muteCount;
|
||||
|
||||
public virtual void Mute()
|
||||
{
|
||||
_muteCount += 1;
|
||||
PropertyChanged?.Invoke(this, new(nameof(IsBusy)));
|
||||
}
|
||||
|
||||
public virtual void Unmute()
|
||||
{
|
||||
_muteCount -= 1;
|
||||
PropertyChanged?.Invoke(this, new(nameof(IsBusy)));
|
||||
if (_muteCount < 0)
|
||||
{
|
||||
#if DEBUG
|
||||
throw new ApplicationException("Mute count becomes negative");
|
||||
#else
|
||||
_muteCount = 0;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
360
Rms.Risk.Mango.Pivot.Core/Models/PivotMetaCache.cs
Normal file
360
Rms.Risk.Mango.Pivot.Core/Models/PivotMetaCache.cs
Normal file
@ -0,0 +1,360 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using log4net;
|
||||
using static Rms.Risk.Mango.Pivot.Core.IPivotTableDataSource;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public static class PivotMetaCache
|
||||
{
|
||||
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!);
|
||||
private static readonly SemaphoreSlim _lock = new(1, 1);
|
||||
|
||||
public const string Any = "<Any>";
|
||||
|
||||
public static async Task<bool> PreloadCollections(this IPivotTableDataSourceMetaProvider pivotService, List<GroupedCollection> collections, string? userEmail = null, CancellationToken token = default)
|
||||
{
|
||||
if ( pivotService == null )
|
||||
throw new ArgumentNullException(nameof(pivotService));
|
||||
if ( collections == null )
|
||||
throw new ArgumentNullException(nameof(collections));
|
||||
|
||||
if ( string.IsNullOrWhiteSpace(pivotService.Prefix) )
|
||||
throw new ArgumentException("Pivot service must have a valid Prefix", nameof(pivotService));
|
||||
|
||||
if ( collections.Count == 0 )
|
||||
{
|
||||
var coll = await LoadCollections(pivotService, token);
|
||||
collections.AddRange(coll);
|
||||
}
|
||||
|
||||
var changed = 0;
|
||||
|
||||
var collectionsToLoad = collections
|
||||
.Where(x =>
|
||||
x is { IsGroup: false }
|
||||
&& x.DataSourcePrefix == pivotService.Prefix
|
||||
&& x.Pivots.Count == 0
|
||||
)
|
||||
.ToList();
|
||||
|
||||
if ( collectionsToLoad.Count == 0 )
|
||||
return false;
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
_log.Debug($"{pivotService.GetType().Name}: Waiting for lock within PreloadCollections...");
|
||||
|
||||
await _lock.WaitAsync(token);
|
||||
|
||||
if ( sw.Elapsed > TimeSpan.FromSeconds(30) )
|
||||
_log.Warn($"Waited {sw.Elapsed} for lock within PreloadCollections");
|
||||
|
||||
try
|
||||
{
|
||||
var tasks = collectionsToLoad.Select(x => LoadPivots(pivotService, x, PivotType.UserAndPredefined, userEmail, token))
|
||||
.ToArray();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
changed += collectionsToLoad.Sum(x => x.Pivots.Count);
|
||||
|
||||
if ( changed > 0 )
|
||||
_log.Debug($"Finished preloading {collections.Count} collections, {changed} pivots for {pivotService.SourceId}");
|
||||
|
||||
return changed > 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
_log.Debug($"{pivotService.GetType().Name}: Released lock within PreloadCollections. Elapsed=\"{sw.Elapsed}\"");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Clear() => _collectionsCache?.Clear();
|
||||
|
||||
public static void Reset(IPivotTableDataSourceMetaProvider pivotService, string collection)
|
||||
{
|
||||
var id = MakeId(pivotService, collection);
|
||||
_collectionsCache?.Remove(id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static async Task<List<GroupedCollection>> LoadCollections(IPivotTableDataSourceMetaProvider pivotService, CancellationToken token = default)
|
||||
{
|
||||
var collectionNames = (await pivotService.GetCollectionsAsync(CollectionType.HaveMeta, token))
|
||||
.Distinct()
|
||||
.OrderBy(x => x)
|
||||
.ToArray()
|
||||
;
|
||||
|
||||
if (collectionNames.Length == 0)
|
||||
throw new ApplicationException("No collections found");
|
||||
|
||||
var d = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var coll in collectionNames)
|
||||
{
|
||||
var s = coll.Split(':');
|
||||
|
||||
string group;
|
||||
string name;
|
||||
|
||||
if (s.Length != 2)
|
||||
{
|
||||
group = pivotService.Prefix;
|
||||
name = coll;
|
||||
}
|
||||
else
|
||||
{
|
||||
group = s[0].Trim();
|
||||
name = s[1].Trim();
|
||||
}
|
||||
|
||||
name = name.Replace("-Meta", "", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
if (d.TryGetValue(group, out var thisGroup))
|
||||
thisGroup.Add(name);
|
||||
else
|
||||
d[group] = [name];
|
||||
}
|
||||
|
||||
var keys = d.Keys.ToArray();
|
||||
Array.Sort(keys);
|
||||
var collections = new List<GroupedCollection>();
|
||||
|
||||
foreach (var k in keys)
|
||||
{
|
||||
collections.Add(new()
|
||||
{
|
||||
DataSourcePrefix = k,
|
||||
CollectionNameWithoutPrefix = k,
|
||||
IsGroup = true
|
||||
});
|
||||
collections.AddRange(d[k]
|
||||
.OrderBy(x => x)
|
||||
.Select(x => new GroupedCollection
|
||||
{
|
||||
DataSourcePrefix = k,
|
||||
CollectionNameWithoutPrefix = x,
|
||||
IsGroup = false
|
||||
}));
|
||||
}
|
||||
|
||||
return collections;
|
||||
}
|
||||
|
||||
private record LoaderArg(
|
||||
IPivotTableDataSourceMetaProvider PivotService,
|
||||
PivotType PivotType,
|
||||
string CollectionNameWithoutPrefix,
|
||||
string? UserName
|
||||
);
|
||||
|
||||
private static ExpiringObjectPool<string, GroupedCollection, LoaderArg>? _collectionsCache;
|
||||
|
||||
private static async Task LoadPivots(
|
||||
IPivotTableDataSourceMetaProvider pivotService,
|
||||
GroupedCollection collection,
|
||||
PivotType pivotType,
|
||||
string? userName = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
if ( collection.IsGroup )
|
||||
return;
|
||||
|
||||
_collectionsCache ??= new(LoadPivotsInternal);
|
||||
|
||||
var id = MakeId(pivotService, collection.CollectionNameWithoutPrefix);
|
||||
|
||||
var coll = await _collectionsCache.Get(id, new (pivotService, PivotType.All, collection.CollectionNameWithoutPrefix, userName), token);
|
||||
collection.CopyFrom(coll);
|
||||
|
||||
var allPivots = coll.Pivots.Where(x => !x.IsGroup).Select(x => x.Pivot).ToList();
|
||||
var predefinedPivots = allPivots.Where(x => !x.Group.StartsWith("User "));
|
||||
var thisUserPivots = string.IsNullOrWhiteSpace(userName) ? [] : allPivots.Where(x => x.Group.StartsWith("User ") && x.Group.EndsWith(userName));
|
||||
|
||||
var res = pivotType switch
|
||||
{
|
||||
PivotType.Predefined => predefinedPivots,
|
||||
PivotType.User => thisUserPivots,
|
||||
PivotType.UserAndPredefined => predefinedPivots.Concat(thisUserPivots),
|
||||
PivotType.All => allPivots,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(pivotType), pivotType, null)
|
||||
};
|
||||
|
||||
var pivots = res
|
||||
.OrderBy(x => (x.Group, x.Name))
|
||||
.ToList()
|
||||
;
|
||||
|
||||
collection.Pivots = MakeGroupedPivots(pivots);
|
||||
}
|
||||
|
||||
private static string MakeId(IPivotTableDataSourceMetaProvider pivotService, string collection) => $"Collection=\"{collection}\" {pivotService.SourceId}";
|
||||
|
||||
private static async Task<GroupedCollection> LoadPivotsInternal(string key, LoaderArg args, CancellationToken token)
|
||||
{
|
||||
var pivotService = args.PivotService;
|
||||
var pivotType = args.PivotType;
|
||||
var userName = args.UserName;
|
||||
|
||||
List<PivotDefinition> ? pivotDefinitions = null;
|
||||
HashSet<string> ? allKeyFields = null;
|
||||
HashSet<string> ? allDataFields = null;
|
||||
string[] ? departments = null;
|
||||
DateTime[] ? cobs = null;
|
||||
PivotColumnDescriptor[]? descriptors = null;
|
||||
|
||||
var collection = new GroupedCollection
|
||||
{
|
||||
CollectionNameWithoutPrefix = args.CollectionNameWithoutPrefix,
|
||||
DataSourcePrefix = args.PivotService.Prefix,
|
||||
IsGroup = false
|
||||
};
|
||||
|
||||
await Task.WhenAll(
|
||||
LoadPivotDefinitions(),
|
||||
LoadCobs(),
|
||||
LoadDepartments(),
|
||||
LoadKeyFields(),
|
||||
LoadDataFields(),
|
||||
LoadColumnDescriptors()
|
||||
);
|
||||
|
||||
var fieldTypes = pivotService.GetFieldTypes(collection.CollectionNameWithoutPrefix);
|
||||
var pivots = MakeGroupedPivots(pivotDefinitions);
|
||||
|
||||
collection.Pivots = pivots;
|
||||
|
||||
if ( allKeyFields != null )
|
||||
collection.KeyFields = allKeyFields;
|
||||
if ( allDataFields != null )
|
||||
collection.DataFields = allDataFields;
|
||||
if ( departments != null )
|
||||
collection.Departments = departments;
|
||||
if ( cobs != null )
|
||||
collection.Cobs = cobs;
|
||||
if ( descriptors != null )
|
||||
collection.ColumnDescriptors = descriptors;
|
||||
if ( fieldTypes.Count > 0 )
|
||||
collection.FieldTypes = fieldTypes;
|
||||
|
||||
return collection;
|
||||
|
||||
async Task LoadPivotDefinitions()
|
||||
{
|
||||
if ( collection.Pivots.Count > 0 )
|
||||
return;
|
||||
pivotDefinitions = await pivotService.GetPivotsAsync(collection.CollectionNameWithoutPrefix, pivotType, userName, token);
|
||||
}
|
||||
|
||||
async Task LoadDepartments()
|
||||
{
|
||||
if ( collection.Departments.Length > 0 )
|
||||
return;
|
||||
departments = (await pivotService.GetDepartmentsAsync(collection.CollectionNameWithoutPrefix, token))
|
||||
.Concat([Any])
|
||||
.ToArray()
|
||||
;
|
||||
}
|
||||
|
||||
async Task LoadKeyFields()
|
||||
{
|
||||
if ( collection.KeyFields.Count > 0 )
|
||||
return;
|
||||
allKeyFields = [..await pivotService.GetKeyFieldsAsync(collection.CollectionNameWithoutPrefix, token)];
|
||||
}
|
||||
|
||||
async Task LoadDataFields()
|
||||
{
|
||||
if ( collection.DataFields.Count > 0 )
|
||||
return;
|
||||
allDataFields = [..await pivotService.GetDataFieldsAsync(collection.CollectionNameWithoutPrefix, token)];
|
||||
}
|
||||
|
||||
async Task LoadCobs()
|
||||
{
|
||||
if ( collection.Cobs.Length > 0 )
|
||||
return;
|
||||
cobs = (await pivotService.GetCobDatesAsync(collection.CollectionNameWithoutPrefix, token: token)).Select(DateTime.Parse).ToArray();
|
||||
}
|
||||
|
||||
async Task LoadColumnDescriptors()
|
||||
{
|
||||
if ( collection.ColumnDescriptors.Length > 0 )
|
||||
return;
|
||||
descriptors = await pivotService.GetColumnDescriptorsAsync(collection.CollectionNameWithoutPrefix, token);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<GroupedPivot> MakeGroupedPivots(List<PivotDefinition>? pivotDefinitions)
|
||||
{
|
||||
if ( pivotDefinitions == null || pivotDefinitions.Count == 0 )
|
||||
{
|
||||
return NoPivotsFound();
|
||||
}
|
||||
|
||||
var d = new Dictionary<string, List<PivotDefinition>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var pivot in pivotDefinitions.Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name != "<Current>"))
|
||||
{
|
||||
var group = pivot.Group;
|
||||
|
||||
if (d.TryGetValue(group, out var thisGroup))
|
||||
thisGroup.Add(pivot);
|
||||
else
|
||||
d[group] = [pivot];
|
||||
}
|
||||
|
||||
var keys = d.Keys.ToArray();
|
||||
Array.Sort(keys);
|
||||
var pivots = new List<GroupedPivot>();
|
||||
|
||||
foreach (var k in keys)
|
||||
{
|
||||
pivots.Add(new() {Text = k, Pivot = new(), IsGroup = true});
|
||||
pivots.AddRange(d[k]
|
||||
.OrderBy(x => x.Name)
|
||||
.Select(x => new GroupedPivot
|
||||
{
|
||||
Text = x.Name,
|
||||
IsGroup = false,
|
||||
Pivot = x
|
||||
}));
|
||||
}
|
||||
|
||||
return pivots.Count == 0
|
||||
? NoPivotsFound()
|
||||
: pivots
|
||||
;
|
||||
}
|
||||
|
||||
private static List<GroupedPivot> NoPivotsFound() =>
|
||||
[
|
||||
new()
|
||||
{
|
||||
IsGroup = true,
|
||||
Text = "No pivots found",
|
||||
Pivot = new()
|
||||
}
|
||||
];
|
||||
}
|
||||
47
Rms.Risk.Mango.Pivot.Core/Models/PivotSettings.cs
Normal file
47
Rms.Risk.Mango.Pivot.Core/Models/PivotSettings.cs
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using Rms.Risk.Mango.Pivot.Core.MongoDb;
|
||||
using NjiAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
|
||||
using TjiAttribute = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class PivotSettings
|
||||
{
|
||||
public class ClickHouseSettings
|
||||
{
|
||||
|
||||
public string ClickHouseUrl { get; set; } = "";
|
||||
public string ClickHouseDatabase { get; set; } = "";
|
||||
public string ClickHouseUser { get; set; } = "";
|
||||
public string ClickHousePassword { get; set; } = "";
|
||||
}
|
||||
|
||||
public int ReloadIntervalMin { get; set; }
|
||||
public string[] PreloadedCollections { get; set; } = [];
|
||||
|
||||
|
||||
public MongoDbConfigRecord? MongoDb { get; set; }
|
||||
public ClickHouseSettings? BFG { get; set; }
|
||||
|
||||
[Nji,Tji] public string ClickHouseUrl => BFG?.ClickHouseUrl ?? "";
|
||||
[Nji,Tji] public string ClickHouseDatabase => BFG?.ClickHouseDatabase ?? "";
|
||||
[Nji,Tji] public string ClickHouseUser => BFG?.ClickHouseUser ?? "";
|
||||
[Nji,Tji] public string ClickHousePassword => BFG?.ClickHousePassword ?? "";
|
||||
}
|
||||
257
Rms.Risk.Mango.Pivot.Core/Models/RolesInfoModel.cs
Normal file
257
Rms.Risk.Mango.Pivot.Core/Models/RolesInfoModel.cs
Normal file
@ -0,0 +1,257 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class RolesInfoModel
|
||||
{
|
||||
public List<RoleInfoModel> Roles { get; set; } = [];
|
||||
|
||||
public static RolesInfoModel FromBson(BsonDocument bsonDocument)
|
||||
{
|
||||
var model = new RolesInfoModel();
|
||||
var rolesArray = bsonDocument["roles"].AsBsonArray;
|
||||
|
||||
foreach (var roleElement in rolesArray)
|
||||
{
|
||||
var roleDoc = roleElement.AsBsonDocument;
|
||||
var role = new RoleInfoModel
|
||||
{
|
||||
RoleName = roleDoc.Contains("role") ? roleDoc["role"].AsString : string.Empty,
|
||||
Db = roleDoc.Contains("db") ? roleDoc["db"].AsString : string.Empty,
|
||||
IsBuiltin = roleDoc.Contains("isBuiltin") && roleDoc["isBuiltin"].AsBoolean,
|
||||
Roles = roleDoc.Contains("roles") && roleDoc["roles"].IsBsonArray
|
||||
? roleDoc["roles"].AsBsonArray.Select(r =>
|
||||
{
|
||||
var roleInDbDoc = r.AsBsonDocument;
|
||||
return new RoleInDbModel
|
||||
{
|
||||
Role = roleInDbDoc.Contains("role") ? roleInDbDoc["role"].AsString : string.Empty,
|
||||
Db = roleInDbDoc.Contains("db") ? roleInDbDoc["db"].AsString : string.Empty
|
||||
};
|
||||
}).ToList()
|
||||
: [],
|
||||
InheritedRoles = roleDoc.Contains("inheritedRoles") && roleDoc["inheritedRoles"].IsBsonArray
|
||||
? roleDoc["inheritedRoles"].AsBsonArray.Select(r => r.ToString()!).ToList()
|
||||
: [],
|
||||
Privileges = roleDoc.Contains("privileges") && roleDoc["privileges"].IsBsonArray
|
||||
? ParsePrivileges(roleDoc["privileges"].AsBsonArray)
|
||||
: [],
|
||||
InheritedPrivileges = roleDoc.Contains("inheritedPrivileges") && roleDoc["inheritedPrivileges"].IsBsonArray
|
||||
? ParsePrivileges(roleDoc["inheritedPrivileges"].AsBsonArray)
|
||||
: []
|
||||
};
|
||||
model.Roles.Add(role);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private static List<PrivilegeModel> ParsePrivileges(BsonArray privilegesArray)
|
||||
{
|
||||
var privileges = new List<PrivilegeModel>();
|
||||
|
||||
foreach (var privilegeElement in privilegesArray)
|
||||
{
|
||||
var privilegeDoc = privilegeElement.AsBsonDocument;
|
||||
var resourceDoc = privilegeDoc["resource"].AsBsonDocument;
|
||||
|
||||
var privilege = new PrivilegeModel
|
||||
{
|
||||
Resource = new()
|
||||
{
|
||||
AnyResource= resourceDoc.Contains("anyResource") && resourceDoc["anyResource"].AsBoolean,
|
||||
Db = resourceDoc.Contains("db") ? resourceDoc["db"].AsString : string.Empty,
|
||||
Collection = resourceDoc.Contains("collection") ? resourceDoc["collection"].AsString : string.Empty,
|
||||
Cluster = resourceDoc.Contains("cluster") && resourceDoc["cluster"].AsBoolean,
|
||||
Buckets = resourceDoc.Contains("system_buckets") ? resourceDoc["system_buckets"].AsString : null,
|
||||
},
|
||||
Actions = privilegeDoc.Contains("actions") && privilegeDoc["actions"].IsBsonArray
|
||||
? privilegeDoc["actions"].AsBsonArray.Select(a => a.AsString).ToList()
|
||||
: []
|
||||
};
|
||||
|
||||
privileges.Add(privilege);
|
||||
}
|
||||
|
||||
return privileges;
|
||||
}
|
||||
}
|
||||
|
||||
public class RoleInfoModel
|
||||
{
|
||||
public override string ToString() => $"{RoleName} ({Db})";
|
||||
|
||||
public string RoleName { get; set; } = string.Empty;
|
||||
public string Db { get; set; } = string.Empty;
|
||||
public bool IsBuiltin { get; set; }
|
||||
public List<RoleInDbModel> Roles { get; set; } = [];
|
||||
public List<PrivilegeModel> Privileges { get; set; } = [];
|
||||
[JsonIgnore]
|
||||
public List<string> InheritedRoles { get; set; } = [];
|
||||
[JsonIgnore]
|
||||
public List<PrivilegeModel> InheritedPrivileges { get; set; } = [];
|
||||
|
||||
public void CopyFrom(RoleInfoModel other)
|
||||
{
|
||||
RoleName = other.RoleName;
|
||||
Db = other.Db;
|
||||
IsBuiltin = other.IsBuiltin;
|
||||
Roles = new(other.Roles);
|
||||
InheritedRoles = new(other.InheritedRoles);
|
||||
Privileges = other.Privileges.Select(p => new PrivilegeModel
|
||||
{
|
||||
Resource = new()
|
||||
{
|
||||
AnyResource = p.Resource.AnyResource,
|
||||
Db = p.Resource.Db,
|
||||
Collection = p.Resource.Collection,
|
||||
Cluster = p.Resource.Cluster,
|
||||
Buckets = p.Resource.Buckets
|
||||
},
|
||||
Actions = [..p.Actions]
|
||||
}).ToList();
|
||||
InheritedPrivileges = other.InheritedPrivileges.Select(p => new PrivilegeModel
|
||||
{
|
||||
Resource = new()
|
||||
{
|
||||
AnyResource = p.Resource.AnyResource,
|
||||
Db = p.Resource.Db,
|
||||
Collection = p.Resource.Collection,
|
||||
Cluster = p.Resource.Cluster,
|
||||
Buckets = p.Resource.Buckets
|
||||
},
|
||||
Actions = [..p.Actions]
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public RoleInfoModel Clone()
|
||||
{
|
||||
var clone = new RoleInfoModel();
|
||||
clone.CopyFrom(this);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public BsonDocument CreateUpdateRoleCommand(bool add)
|
||||
{
|
||||
var command = new BsonDocument
|
||||
{
|
||||
{ add ? "createRole" : "updateRole", RoleName },
|
||||
{ "privileges", new BsonArray(
|
||||
Privileges.Select(p => new BsonDocument
|
||||
{
|
||||
{ "resource", ToBsonDocument(p.Resource) },
|
||||
{ "actions", new BsonArray(p.Actions) }
|
||||
})
|
||||
) },
|
||||
{ "roles", new BsonArray(
|
||||
Roles.Select(x =>
|
||||
string.IsNullOrWhiteSpace(x.Db)
|
||||
? BsonValue.Create(x.Role)
|
||||
: new BsonDocument
|
||||
{
|
||||
{ "role", x.Role },
|
||||
{ "db", x.Db }
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
// { "authenticationRestrictions", new BsonArray(
|
||||
// role.AuthenticationRestrictions.Select(ar => new BsonDocument
|
||||
// {
|
||||
// { "clientSource", new BsonArray(ar.ClientSource) },
|
||||
// { "serverAddress", new BsonArray(ar.ServerAddress) }
|
||||
// })
|
||||
// ) },
|
||||
{ "writeConcern", new BsonDocument { { "w", "majority" } } }
|
||||
};
|
||||
return command;
|
||||
}
|
||||
|
||||
private static BsonDocument ToBsonDocument(ResourceModel resource)
|
||||
{
|
||||
var resourceDoc = new BsonDocument();
|
||||
|
||||
if (resource.Cluster)
|
||||
{
|
||||
resourceDoc.Add("cluster", true);
|
||||
}
|
||||
else if (resource.AnyResource)
|
||||
{
|
||||
resourceDoc.Add("anyResource", true);
|
||||
}
|
||||
else if ( string.IsNullOrWhiteSpace(resource.Buckets) )
|
||||
{
|
||||
resourceDoc.Add("db", resource.Db);
|
||||
resourceDoc.Add("collection", resource.Collection);
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceDoc.Add("system_buckets", resource.Buckets);
|
||||
}
|
||||
|
||||
return resourceDoc;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class RoleInDbModel
|
||||
{
|
||||
public string Db { get; set; } = string.Empty;
|
||||
public string Role { get; set; } = string.Empty;
|
||||
|
||||
public override string ToString() => $"{Role}, {Db}";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not RoleInDbModel other)
|
||||
return false;
|
||||
|
||||
return Db == other.Db && Role == other.Role;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Db, Role);
|
||||
}
|
||||
}
|
||||
|
||||
public class PrivilegeModel
|
||||
{
|
||||
public override string ToString() => $"{Resource} => {string.Join(", ", Actions)}";
|
||||
|
||||
public ResourceModel Resource { get; set; } = new();
|
||||
public List<string> Actions { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ResourceModel
|
||||
{
|
||||
public override string ToString() => $"Db={Db}, Collection={Collection}, Any={AnyResource}, Cluster={Cluster} {Buckets}";
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public bool AnyResource { get; set; }
|
||||
public string Db { get; set; } = string.Empty;
|
||||
public string Collection { get; set; } = string.Empty;
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public bool Cluster { get; set;}
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string? Buckets { get; set; }
|
||||
}
|
||||
90
Rms.Risk.Mango.Pivot.Core/Models/SimpleTranspose.cs
Normal file
90
Rms.Risk.Mango.Pivot.Core/Models/SimpleTranspose.cs
Normal file
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public static class SimpleTranspose
|
||||
{
|
||||
|
||||
public static IPivotedData? Transpose(IPivotedData? src)
|
||||
{
|
||||
if ( src == null
|
||||
|| src.Count < 1
|
||||
|| src.Headers.Count < 1
|
||||
|| src is { Count: 1, Headers.Count: 1 }
|
||||
)
|
||||
{
|
||||
return src;
|
||||
}
|
||||
|
||||
var columnHeaders = GetColHeaders(src);
|
||||
if ( columnHeaders == null || columnHeaders.Count != src.Count+1 )
|
||||
return src;
|
||||
|
||||
var headers = columnHeaders
|
||||
.OrderBy(x => x.Value)
|
||||
.Select(x => x.Key)
|
||||
.ToArray();
|
||||
|
||||
var dest = new ArrayBasedPivotData(headers);
|
||||
|
||||
var srcHeaders = src.Headers;
|
||||
|
||||
|
||||
// row and col corresponds to dest
|
||||
for ( var row = 0; row < srcHeaders.Count-1; row++ )
|
||||
{
|
||||
var r = new object?[src.Count+1];
|
||||
// first column is src.Headers
|
||||
r[0] = srcHeaders.Skip(row+1).First(); // [row+1]
|
||||
// the rest (1st src column becomes dest row header)
|
||||
for ( var col = 1; col < headers.Length; col++ )
|
||||
r[col] = src.Get(row+1, col-1); // note reverse row/col
|
||||
dest.Add(r);
|
||||
}
|
||||
|
||||
// double check
|
||||
if ( dest.Count != src.Headers.Count-1
|
||||
|| dest.Headers.Count != src.Count+1 )
|
||||
{
|
||||
return src;
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
private static Dictionary<string, int>? GetColHeaders(IPivotedData src)
|
||||
{
|
||||
var headers = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[src.Headers.First() ?? string.Empty] = 0
|
||||
};
|
||||
|
||||
for ( var i = 0; i < src.Count; i++ )
|
||||
{
|
||||
var key = src.Get(0, i)?.ToString() ?? string.Empty;
|
||||
if ( key == string.Empty )
|
||||
key = "-";
|
||||
|
||||
if ( !headers.TryAdd(key, i+1) )
|
||||
return null;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
43
Rms.Risk.Mango.Pivot.Core/Models/SingleFieldMapping.cs
Normal file
43
Rms.Risk.Mango.Pivot.Core/Models/SingleFieldMapping.cs
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public class SingleFieldMapping : ICloneable
|
||||
{
|
||||
public override string ToString() => $"{Id} {Type} {Purpose}";
|
||||
|
||||
public int Id { get; set; }
|
||||
public PivotFieldPurpose Purpose { get; set; }
|
||||
|
||||
[BsonIgnore]
|
||||
public Type Type { get; set; } = typeof(object);
|
||||
|
||||
public string TypeName
|
||||
{
|
||||
get => Type.FullName!;
|
||||
set => Type = Type.GetType( value ) ?? typeof(object);
|
||||
}
|
||||
|
||||
|
||||
object ICloneable. Clone() => Clone();
|
||||
public SingleFieldMapping Clone() => (SingleFieldMapping)MemberwiseClone();
|
||||
}
|
||||
295
Rms.Risk.Mango.Pivot.Core/Models/TransposedPivotData.cs
Normal file
295
Rms.Risk.Mango.Pivot.Core/Models/TransposedPivotData.cs
Normal file
@ -0,0 +1,295 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class TransposedPivotData : NotifyPropertyChangedBase, IPivotedData
|
||||
{
|
||||
private readonly IPivotedData _data;
|
||||
private IPivotedData? _transposedData;
|
||||
|
||||
public TransposedPivotData( IPivotedData data, string columnHeaderColumn, string [] rowHeaderColumns,
|
||||
string dataColumn, CancellationToken token )
|
||||
{
|
||||
_data = data;
|
||||
_columnHeaderColumn = columnHeaderColumn;
|
||||
_rowHeaderColumns = rowHeaderColumns;
|
||||
_dataColumn = dataColumn;
|
||||
_transposedData = CreateTransposedTable( token );
|
||||
Id = $"{data.Id}-Transposed";
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
private string [] _rowHeaderColumns;
|
||||
public string [] RowHeaderColumn
|
||||
{
|
||||
get => _rowHeaderColumns;
|
||||
set
|
||||
{
|
||||
if (_rowHeaderColumns == value)
|
||||
return;
|
||||
_rowHeaderColumns = value;
|
||||
Refresh();
|
||||
OnPropertyChanged(() => RowHeaderColumn);
|
||||
}
|
||||
}
|
||||
|
||||
private string _columnHeaderColumn;
|
||||
public string ColumnHeaderColumn
|
||||
{
|
||||
get => _columnHeaderColumn;
|
||||
set
|
||||
{
|
||||
if (_columnHeaderColumn == value)
|
||||
return;
|
||||
_columnHeaderColumn = value;
|
||||
Refresh();
|
||||
OnPropertyChanged(() => ColumnHeaderColumn);
|
||||
}
|
||||
}
|
||||
|
||||
private string _dataColumn;
|
||||
public string DataColumn
|
||||
{
|
||||
get => _dataColumn;
|
||||
set
|
||||
{
|
||||
if (_dataColumn == value)
|
||||
return;
|
||||
_dataColumn = value;
|
||||
Refresh();
|
||||
OnPropertyChanged(() => DataColumn);
|
||||
}
|
||||
}
|
||||
|
||||
#region IPivotedData
|
||||
|
||||
public IReadOnlyCollection<string> Headers => _transposedData?.Headers ?? _data.Headers;
|
||||
public int Count => _transposedData?.Count ?? _data.Count;
|
||||
|
||||
public Dictionary<string, int> GetColumnPositions()
|
||||
{
|
||||
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// this is safer than calling ToDictionary as it handles duplicate headers
|
||||
foreach ( var (key, pos) in Headers.Select((x, i) => (Key: x, Value: i)) )
|
||||
{
|
||||
dict.TryAdd(key, pos);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
public object? Get( int col, int row )
|
||||
{
|
||||
if ( _transposedData != null )
|
||||
{
|
||||
if ( col >= _transposedData.Headers.Count
|
||||
|| row >= _transposedData.Count )
|
||||
return null;
|
||||
return _transposedData.Get( col, row );
|
||||
}
|
||||
|
||||
if (col >= _data.Headers.Count
|
||||
|| row >= _data.Count)
|
||||
return null;
|
||||
|
||||
return _data.Get( col, row );
|
||||
}
|
||||
|
||||
public Type GetColumnType( int col ) =>
|
||||
_transposedData != null
|
||||
? _transposedData.GetColumnType( col )
|
||||
: _data.GetColumnType( col );
|
||||
|
||||
public IPivotedData Filter(Func<int, bool> filter)
|
||||
{
|
||||
var data = _data.Filter(filter);
|
||||
return new TransposedPivotData(data,
|
||||
_columnHeaderColumn,
|
||||
_rowHeaderColumns,
|
||||
_dataColumn,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
private async void Refresh()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cancellationTokenSource.CancelAsync();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
var token = _cancellationTokenSource.Token;
|
||||
var data = await Task.Run( () => CreateTransposedTable(token), token );
|
||||
if ( data == null )
|
||||
return;
|
||||
_transposedData = data;
|
||||
OnPropertyChanged( () => Headers );
|
||||
OnPropertyChanged( () => Count );
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayBasedPivotData? CreateTransposedTable( CancellationToken token )
|
||||
{
|
||||
if ( token.IsCancellationRequested )
|
||||
return null;
|
||||
|
||||
if ( _data == null
|
||||
|| _data.Headers.Count == 0
|
||||
|| _data.Count == 0 )
|
||||
return null;
|
||||
|
||||
var rhc = RowHeaderColumn;
|
||||
var chc = ColumnHeaderColumn;
|
||||
var dc = DataColumn;
|
||||
|
||||
if ( rhc == null
|
||||
|| rhc.Length == 0
|
||||
|| string.IsNullOrWhiteSpace( chc )
|
||||
|| string.IsNullOrWhiteSpace( dc )
|
||||
)
|
||||
return null;
|
||||
|
||||
if ( rhc.Any( x => !_data.Headers.Contains( x ) )
|
||||
|| !_data.Headers.Contains( chc )
|
||||
|| !_data.Headers.Contains( dc )
|
||||
)
|
||||
return null;
|
||||
|
||||
var headersDict = _data.Headers.Select((s, i) => (s, i)).ToDictionary(x => x.s, x => x.i);
|
||||
|
||||
var columns = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var colPos = headersDict[chc];
|
||||
var count = rhc.Length;
|
||||
|
||||
for ( var row = 0; row < _data.Count; row++ )
|
||||
{
|
||||
var v = ConvertToString( _data.Get( colPos, row ) );
|
||||
|
||||
if ( columns.ContainsKey( v ) )
|
||||
continue;
|
||||
|
||||
columns[v] = count++;
|
||||
}
|
||||
|
||||
if ( token.IsCancellationRequested )
|
||||
return null;
|
||||
|
||||
var rows = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var rowPos = rhc.Select( x => headersDict[x]).ToArray();
|
||||
count = 0;
|
||||
|
||||
for ( var row = 0; row < _data.Count; row++ )
|
||||
{
|
||||
var r = row;
|
||||
var v = string.Join("|", rowPos.Select( x => ConvertToString( _data.Get( x, r ) )));
|
||||
|
||||
if ( rows.ContainsKey( v ) )
|
||||
continue;
|
||||
|
||||
rows[v] = count++;
|
||||
}
|
||||
|
||||
if ( token.IsCancellationRequested )
|
||||
return null;
|
||||
|
||||
var d = new ArrayBasedPivotData(
|
||||
rhc
|
||||
.Concat( columns
|
||||
.OrderBy( x => x.Value )
|
||||
.Select( x => x.Key )
|
||||
)
|
||||
);
|
||||
|
||||
// preallocate space
|
||||
|
||||
for ( var row = 0; row < rows.Count; row++ )
|
||||
{
|
||||
d.Add( new object[columns.Count+rhc.Length] );
|
||||
var k = rows.First( x => x.Value == row ).Key.Split( '|' );
|
||||
for ( var col = 0; col < k.Length; col++ )
|
||||
d[col, row] = k[col];
|
||||
}
|
||||
|
||||
if ( token.IsCancellationRequested )
|
||||
return null;
|
||||
|
||||
var dataPos = headersDict[dc];
|
||||
|
||||
for ( var row = 0; row < _data.Count; row++ )
|
||||
{
|
||||
if ( row % 2048 == 0)
|
||||
if ( token.IsCancellationRequested )
|
||||
return null;
|
||||
|
||||
var destCol = columns[ConvertToString(_data.Get( colPos, row ))];
|
||||
|
||||
var r = row;
|
||||
var v = string.Join("|", rowPos.Select( x => ConvertToString( _data.Get( x, r ) )));
|
||||
var destRow = rows[v];
|
||||
|
||||
|
||||
var dest = d[destCol, destRow];
|
||||
var src = _data.Get( dataPos, row );
|
||||
|
||||
if ( src == null )
|
||||
continue;
|
||||
if (dest == null)
|
||||
d[destCol, destRow] = src;
|
||||
else
|
||||
{
|
||||
switch ( dest )
|
||||
{
|
||||
case double dbl:
|
||||
dest = dbl + Convert.ToDouble( src );
|
||||
break;
|
||||
case int i:
|
||||
dest = i + Convert.ToInt32( src );
|
||||
break;
|
||||
case long l:
|
||||
dest = l + Convert.ToInt64( src );
|
||||
break;
|
||||
case decimal dec:
|
||||
dest = dec + Convert.ToInt64( src );
|
||||
break;
|
||||
default:
|
||||
dest = src;
|
||||
break;
|
||||
}
|
||||
d[destCol, destRow] = dest;
|
||||
}
|
||||
}
|
||||
|
||||
return token.IsCancellationRequested ? null : d;
|
||||
}
|
||||
|
||||
private static string ConvertToString( object? val )
|
||||
{
|
||||
var v = val is DateTime time ? time.ToString( "yyyy-MM-dd" ) : val?.ToString() ?? "";
|
||||
return v;
|
||||
}
|
||||
}
|
||||
103
Rms.Risk.Mango.Pivot.Core/Models/UserInfoModel.cs
Normal file
103
Rms.Risk.Mango.Pivot.Core/Models/UserInfoModel.cs
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.Models;
|
||||
|
||||
public class UserInfoModel
|
||||
{
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string Db { get; set; } = string.Empty;
|
||||
[JsonIgnore]
|
||||
public bool IsBuiltin { get; set; }
|
||||
[JsonIgnore]
|
||||
public string? Password { get; set; }
|
||||
public List<RoleInDbModel> Roles { get; set; } = [];
|
||||
|
||||
public static UserInfoModel FromBson(BsonDocument bsonDocument) =>
|
||||
new()
|
||||
{
|
||||
UserName = bsonDocument.GetValue("user", string.Empty).AsString,
|
||||
Db = bsonDocument.GetValue("db", string.Empty).AsString,
|
||||
IsBuiltin = false,
|
||||
Password = null,
|
||||
Roles = bsonDocument.Contains("roles")
|
||||
? bsonDocument["roles"].AsBsonArray
|
||||
.Select(role => new RoleInDbModel
|
||||
{
|
||||
Db = role["db"].AsString,
|
||||
Role = role["role"].AsString
|
||||
}).ToList()
|
||||
: []
|
||||
};
|
||||
|
||||
public void CopyFrom(UserInfoModel other)
|
||||
{
|
||||
if (other == null) throw new ArgumentNullException(nameof(other));
|
||||
|
||||
UserName = other.UserName;
|
||||
Db = other.Db;
|
||||
IsBuiltin = other.IsBuiltin;
|
||||
Password = other.Password;
|
||||
Roles = other.Roles.Select(role => new RoleInDbModel
|
||||
{
|
||||
Db = role.Db,
|
||||
Role = role.Role
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public UserInfoModel Clone()
|
||||
{
|
||||
var clone = new UserInfoModel();
|
||||
clone.CopyFrom(this);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class UsersModel
|
||||
{
|
||||
public List<UserInfoModel> Users { get; set; } = [];
|
||||
|
||||
public static UsersModel FromBson(BsonDocument bsonDocument)
|
||||
{
|
||||
var model = new UsersModel();
|
||||
var usersArray = bsonDocument["users"].AsBsonArray;
|
||||
foreach (var userElement in usersArray)
|
||||
{
|
||||
var userDoc = userElement.AsBsonDocument;
|
||||
var user = UserInfoModel.FromBson(userDoc);
|
||||
model.Users.Add(user);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
public void CopyFrom(UsersModel other)
|
||||
{
|
||||
Users = other.Users.Select(user => user.Clone()).ToList();
|
||||
}
|
||||
|
||||
public UsersModel Clone()
|
||||
{
|
||||
var clone = new UsersModel();
|
||||
clone.CopyFrom(this);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
336
Rms.Risk.Mango.Pivot.Core/MongoDb/AdminServiceExtensions.cs
Normal file
336
Rms.Risk.Mango.Pivot.Core/MongoDb/AdminServiceExtensions.cs
Normal file
@ -0,0 +1,336 @@
|
||||
/*
|
||||
* dbMango
|
||||
*
|
||||
* Copyright 2025 Deutsche Bank AG
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using MongoDB.Bson;
|
||||
using Rms.Risk.Mango.Pivot.Core.Models;
|
||||
using Rms.Risk.Mango.Services;
|
||||
using Rms.Risk.Mango.Services.Models;
|
||||
|
||||
namespace Rms.Risk.Mango.Pivot.Core.MongoDb;
|
||||
|
||||
public class ListDatabasesResultItem
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public long Size { get; set; }
|
||||
public bool IsEmpty { get; set; }
|
||||
}
|
||||
|
||||
public class CollectionStats
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
public long Size { get; init; }
|
||||
public long Count { get; init; }
|
||||
public long StorageSize { get; init; }
|
||||
public long TotalIndexSize { get; init; }
|
||||
public long TotalSize { get; init; }
|
||||
public bool Sharded { get; init; }
|
||||
public BsonDocument? Details { get; set; } = new();
|
||||
}
|
||||
|
||||
public static class AdminServiceExtensions
|
||||
{
|
||||
public static async Task<List<ListDatabasesResultItem>> ListDatabases(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var dbs = await service.RunCommand(new ("listDatabases", 1), token);
|
||||
|
||||
return dbs["databases"].AsBsonArray
|
||||
.Select(x => new ListDatabasesResultItem
|
||||
{
|
||||
Name = x["name"].ToString() ?? "???",
|
||||
Size = x["sizeOnDisk"].ToInt64(),
|
||||
IsEmpty = x["empty"].ToBoolean()
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static async Task<string> GetVersion(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var res = await service.RunCommand(new ("buildInfo", 1), token);
|
||||
|
||||
return res["version"].ToString() ?? "0.0.0";
|
||||
}
|
||||
|
||||
//public static async Task<bool> IsSharded( this IMongoDbDatabaseAdminService db, string database, string collectionName )
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// var command = BsonDocument.Parse(
|
||||
// $"{{ getShardVersion: \"{database}.{collectionName}\" }}"
|
||||
// );
|
||||
// var doc = await db.RunCommand( command );
|
||||
// return (int)(doc["ok"].ToDouble()) == 1;
|
||||
// }
|
||||
// catch ( Exception )
|
||||
// {
|
||||
// //if ( ex.Message.Contains( "is not sharded" ) || ex.Message.Contains( "does not have a routing table" ) )
|
||||
// return false;
|
||||
// }
|
||||
//}
|
||||
|
||||
public static async Task<bool> IsSharded( this IMongoDbDatabaseAdminService db, string database, string collectionName )
|
||||
{
|
||||
var stats = await db.CollStats($"{database}.{collectionName}" );
|
||||
return stats.Sharded;
|
||||
}
|
||||
|
||||
public static async Task<bool> IsSharded( this IMongoDbDatabaseAdminService db, string collectionName )
|
||||
{
|
||||
var stats = await db.CollStats(collectionName);
|
||||
return stats.Sharded;
|
||||
}
|
||||
|
||||
public static async Task<CollectionStats> CollStats(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
string collectionName,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var command = new BsonDocument
|
||||
{
|
||||
{ "collStats", collectionName },
|
||||
{ "scale", 1 },
|
||||
};
|
||||
var res = await service.RunCommand(command, token);
|
||||
|
||||
var stats = new CollectionStats
|
||||
{
|
||||
Name = res["ns"].ToString() ?? "",
|
||||
Size = res["size"].ToInt64(),
|
||||
Count = res["count"].ToInt64(),
|
||||
StorageSize = res["storageSize"].ToInt64(),
|
||||
TotalIndexSize = res["totalIndexSize"].ToInt64(),
|
||||
TotalSize = res["totalSize"].ToInt64(),
|
||||
Sharded = res.Contains("sharded") && res["sharded"].ToBoolean(),
|
||||
Details = res
|
||||
};
|
||||
return stats;
|
||||
}
|
||||
|
||||
public static async Task<CollStatsModel> CollStatsDetailed(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
string collectionName,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var command = new BsonDocument
|
||||
{
|
||||
{ "collStats", collectionName },
|
||||
{ "scale", 1 },
|
||||
};
|
||||
var res = await service.RunCommand(command, token);
|
||||
var json = res.ToJson();
|
||||
return CollStatsModel.FromJson(json);
|
||||
}
|
||||
|
||||
public static async Task<DatabaseStatsModel> DbStats(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var command = new BsonDocument
|
||||
{
|
||||
{ "dbStats", 1 },
|
||||
{ "freeStorage", 1 },
|
||||
};
|
||||
var res = await service.RunCommand(command, token);
|
||||
return DatabaseStatsModel.FromBson(res);
|
||||
}
|
||||
|
||||
public static async Task DropRole(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
string roleName,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var command = new BsonDocument
|
||||
{
|
||||
{ "dropRole", roleName },
|
||||
{ "writeConcern",new BsonDocument
|
||||
{
|
||||
{ "w", "majority" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await service.RunCommand(command, token);
|
||||
}
|
||||
|
||||
public static async Task CreateRole(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
RoleInfoModel role,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var command = role.CreateUpdateRoleCommand(true);
|
||||
await service.RunCommand(command, token);
|
||||
}
|
||||
|
||||
public static async Task UpdateRole(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
RoleInfoModel role,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var command = role.CreateUpdateRoleCommand(false);
|
||||
await service.RunCommand(command, token);
|
||||
}
|
||||
|
||||
|
||||
public static async Task<RolesInfoModel> GetRolesInfo(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
string? roleName = null,
|
||||
string? dbName = null,
|
||||
bool showBuiltInRoles = true,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
BsonDocument command;
|
||||
if ( string.IsNullOrWhiteSpace(roleName) )
|
||||
{
|
||||
command = new ()
|
||||
{
|
||||
{ "rolesInfo", 1 },
|
||||
{ "showPrivileges", true },
|
||||
{ "showBuiltinRoles", showBuiltInRoles }
|
||||
};
|
||||
}
|
||||
else if (dbName == null)
|
||||
{
|
||||
command = new()
|
||||
{
|
||||
{ "rolesInfo", roleName },
|
||||
{ "showPrivileges", true },
|
||||
{ "showBuiltinRoles", showBuiltInRoles }
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
command = new()
|
||||
{
|
||||
{ "rolesInfo", new BsonDocument
|
||||
{
|
||||
{ "role", roleName },
|
||||
{ "db", dbName }
|
||||
}
|
||||
},
|
||||
{ "showPrivileges", true },
|
||||
{ "showBuiltinRoles", showBuiltInRoles }
|
||||
};
|
||||
}
|
||||
|
||||
var res = await service.RunCommand(command, token);
|
||||
return RolesInfoModel.FromBson(res);
|
||||
}
|
||||
|
||||
public static async Task<IndexesInfoModel> GetIndexes(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
string collectionName,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var command = new BsonDocument()
|
||||
{
|
||||
{ "listIndexes", collectionName }
|
||||
};
|
||||
|
||||
var res = await service.RunCommand(command, token);
|
||||
return IndexesInfoModel.FromBson(res);
|
||||
}
|
||||
|
||||
public static async Task<UsersModel> GetUsersInfo(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var command = new BsonDocument()
|
||||
{
|
||||
{ "usersInfo", 1 }
|
||||
};
|
||||
|
||||
var res = await service.RunCommand(command, token);
|
||||
return UsersModel.FromBson(res);
|
||||
}
|
||||
|
||||
public static async Task DropUser(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
string userName,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var command = new BsonDocument()
|
||||
{
|
||||
{ "dropUser", userName },
|
||||
{ "writeConcern",new BsonDocument
|
||||
{
|
||||
{ "w", "majority" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_ = await service.RunCommand(command, token);
|
||||
}
|
||||
|
||||
public static async Task<string[]> GetLogCategories(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var command = new BsonDocument
|
||||
{
|
||||
{ "getLog", "*" }
|
||||
};
|
||||
var doc = await service.RunCommand(command, token);
|
||||
|
||||
var logs = doc.Contains("names") ? doc["names"].AsBsonArray : new();
|
||||
|
||||
return logs.Where(log => log.IsString).Select(log => log.AsString).ToArray();
|
||||
}
|
||||
|
||||
public static async Task<List<LogRecordModel>> GetLogs(
|
||||
this IMongoDbDatabaseAdminService service,
|
||||
string logCategory = "global",
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var command = new BsonDocument
|
||||
{
|
||||
{ "getLog", logCategory }
|
||||
};
|
||||
var doc = await service.RunCommand(command, token);
|
||||
|
||||
var logs = doc.Contains("log") ? doc["log"].AsBsonArray : new();
|
||||
var res = new List<LogRecordModel>();
|
||||
|
||||
foreach (var log in logs)
|
||||
{
|
||||
if (log is BsonDocument d)
|
||||
{
|
||||
res.Add(LogRecordModel.FromBson(d));
|
||||
}
|
||||
else if (log.IsString)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bson = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>(log.AsString);
|
||||
res.Add(LogRecordModel.FromBson(bson));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user