diff --git a/.env.example b/.env.example index 36797ac..3a25298 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,9 @@ APP_DEBUG="true" DB_DRIVER="sqlite" DB_DSN="sqlite/testdb.db?cache=shared&mode=memory" + +# Status Mapping Configuration (JSON format) +# Maps database statuses to classified statuses for API responses +# Default mappings: unlisted->removed, yanked->removed, deleted->deleted, +# deprecated->deprecated, unpublished->removed, archived->deprecated, active->active +# STATUS_MAPPING='{"unlisted":"removed","yanked":"removed","deleted":"deleted","deprecated":"deprecated","unpublished":"removed","archived":"deprecated","active":"active"}' diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 17efe83..ccc5f0a 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -14,12 +14,12 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 # Get tags to allow build script to get build version + fetch-depth: 0 # Get tags to allow a build script to get a build version - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: 'go.mod' - name: Build run: make build_amd diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index a5a401b..7a0092d 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -14,17 +14,15 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 # Get tags to allow build script to get build version + fetch-depth: 0 # Get tags to allow a build script to get a build version - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: 'go.mod' - name: Setup Version run: make version - name: golangci-lint - uses: golangci/golangci-lint-action@v6 - with: - args: --timeout 5m + uses: golangci/golangci-lint-action@v9 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f68af98..bc088ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,12 +12,13 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 # Get tags to allow build script to get build version + ref: ${{ github.ref }} # Checkout the specified tag + fetch-depth: 0 # Get tags to allow a build script to get a build version - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: 'go.mod' - name: Build run: | diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d63dd41 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,98 @@ +version: "2" + +run: + timeout: 5m + tests: true + +formatters: + enable: + - gci + - goimports + + settings: + gci: + sections: + - standard + - default + +linters: + enable: + - cyclop + - errname + - exhaustive + - funlen + - gocognit + - goconst + - gocritic + - godot + - gosec + - lll + - loggercheck + - makezero + - nakedret + - nilerr + - nilnil + - nolintlint + - nonamedreturns + - predeclared + - reassign + - staticcheck + - unconvert + - unparam + - usestdlibvars + - whitespace + + settings: + cyclop: + max-complexity: 30 + package-average: 10.0 + + errcheck: + check-type-assertions: true + + exhaustive: + check: + - switch + - map + + funlen: + lines: 150 + statements: 80 + + gocognit: + min-complexity: 40 + + gosec: + excludes: + - G117 + - G304 + + govet: + enable-all: true + disable: + - fieldalignment + settings: + shadow: + strict: true + + nakedret: + max-func-lines: 10 + + lll: + line-length: 180 + + staticcheck: + checks: ["all", "-SA1019"] + + exclusions: + paths: + - tests + rules: + - path: _test\.go + linters: + - gocognit + - govet + - cyclop + - godot + - funlen + - lll \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d6cecd2..7a4ca75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Upcoming changes... + +## [0.8.0] - 2026-03-20 +### Added +- Added `GetComponentStatus`/`GetComponentsStatus` services for getting single and multiple development life-cycle information +- Added support for custom status mapping for _retrieved_ and _registry-specific_ status +### Changed +- Using **go-component-helper** to get always the right component version based on user request + + ## [0.7.0] - 2026-01-30 ### Added - Added database version info (`schema_version`, `created_at`) to `StatusResponse` across all component service endpoints @@ -38,7 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.0.1] - ? ### Added - ? - +[0.8.0]: https://github.com/scanoss/components/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/scanoss/components/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/scanoss/components/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/scanoss/components/compare/v0.4.0...v0.5.0 diff --git a/Makefile b/Makefile index 813e919..b18a6ed 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ - +## Constants +# Linter version +LINT_VERSION := v2.10.1 #vars IMAGE_NAME=scanoss-components REPO=scanoss @@ -39,7 +41,10 @@ lint_local_fix: ## Run local instance of linting across the code base including golangci-lint run --fix ./... lint_docker: ## Run docker instance of linting across the code base - docker run --rm -v $(pwd):/app -v ~/.cache/golangci-lint/v1.50.1:/root/.cache -w /app golangci/golangci-lint:v1.50.1 golangci-lint run ./... + docker run --rm -v $(PWD):/app -v ~/.cache/golangci-lint/$(LINT_VERSION):/root/.cache -w /app golangci/golangci-lint:$(LINT_VERSION) golangci-lint run ./... + +lint_docker_fix: ## Run docker instance of linting across the code base auto-fixing + docker run --rm -v $(PWD):/app -v ~/.cache/golangci-lint/$(LINT_VERSION):/root/.cache -w /app golangci/golangci-lint:$(LINT_VERSION) golangci-lint run --fix ./... run_local: ## Launch the API locally for test @echo "Launching API locally..." diff --git a/README.md b/README.md index 1765f4d..0c11523 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,12 @@ DB_SCHEMA=scanoss DB_SSL_MODE=disable DB_DSN= ``` +## Status mapping +User can define custom status mapping using `STATUS_MAPPING` variable (JSON format) +``` bash +STATUS_MAPPING='{"unlisted":"removed","yanked":"removed","deleted":"deleted","deprecated":"deprecated","unpublished":"removed","archived":"deprecated","active":"active"}' +``` ## Docker Environment diff --git a/cmd/server/main.go b/cmd/server/main.go index 77547cb..bc5d51f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -20,10 +20,11 @@ package main import ( "fmt" "os" + "scanoss.com/components/pkg/cmd" ) -// main starts the gRPC Component Service +// main starts the gRPC Component Service. func main() { // Launch the Component Server Service if err := cmd.RunServer(); err != nil { diff --git a/config/app-config-dev.json b/config/app-config-dev.json index 4cb5440..5f715f8 100644 --- a/config/app-config-dev.json +++ b/config/app-config-dev.json @@ -10,5 +10,16 @@ "User": "scanoss", "Passwd": "secret123!", "Schema": "scanoss" + }, + "StatusMapping": { + "Mapping": { + "unlisted": "removed", + "yanked": "removed", + "deleted": "deleted", + "deprecated": "deprecated", + "unpublished": "removed", + "archived": "deprecated", + "active": "active" + } } } diff --git a/go.mod b/go.mod index db385fb..568a405 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,33 @@ module scanoss.com/components -go 1.24 - -toolchain go1.24.6 +go 1.25.0 require ( github.com/golobby/config/v3 v3.4.2 github.com/google/go-cmp v0.7.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/jmoiron/sqlx v1.4.0 - github.com/lib/pq v1.10.9 - github.com/scanoss/go-grpc-helper v0.11.0 - github.com/scanoss/go-models v0.4.0 - github.com/scanoss/go-purl-helper v0.2.1 - github.com/scanoss/papi v0.28.0 + github.com/lib/pq v1.12.0 + github.com/scanoss/go-component-helper v0.5.0 + github.com/scanoss/go-grpc-helper v0.13.0 + github.com/scanoss/go-models v0.7.0 + github.com/scanoss/go-purl-helper v0.3.0 + github.com/scanoss/papi v0.33.0 github.com/scanoss/zap-logging-helper v0.4.0 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/metric v1.38.0 - go.uber.org/zap v1.27.0 - google.golang.org/grpc v1.75.0 - modernc.org/sqlite v1.38.2 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/metric v1.42.0 + go.uber.org/zap v1.27.1 + google.golang.org/grpc v1.79.3 + modernc.org/sqlite v1.47.0 ) // replace github.com/scanoss/papi => ../papi require ( github.com/BurntSushi/toml v1.2.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -35,39 +36,42 @@ require ( github.com/golobby/dotenv v1.3.2 // indirect github.com/golobby/env/v2 v2.2.4 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/package-url/packageurl-go v0.1.3 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/package-url/packageurl-go v0.1.5 // indirect github.com/phuslu/iploc v1.0.20230201 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/scanoss/ipfilter/v2 v2.0.2 // indirect github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.37.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.26.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.66.3 // indirect + modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) // Details of how to use the "replace" command for local development // https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive -// ie. replace github.com/scanoss/papi => ../papi +// ie. +//replace github.com/scanoss/papi => ../papi + +//replace github.com/scanoss/go-component-helper/componenthelper => ../go-component-helper + // require github.com/scanoss/papi v0.0.0-unpublished //replace github.com/scanoss/go-grpc-helper v0.6.0 => ../go-grpc-helper diff --git a/go.sum b/go.sum index b4c5901..6666116 100644 --- a/go.sum +++ b/go.sum @@ -391,17 +391,21 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -562,10 +566,13 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vb github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -585,19 +592,20 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= +github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= -github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= +github.com/package-url/packageurl-go v0.1.5 h1:O4efRXja2XQ5CtiiYiCZ22k/m7i5ugLiAghgcC+eDgk= +github.com/package-url/packageurl-go v0.1.5/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= github.com/phuslu/iploc v1.0.20230201 h1:AMhy7j8z0N5iI0jaqh514KTDEB7wVdQJ4Y4DJPCvKBU= github.com/phuslu/iploc v1.0.20230201/go.mod h1:gsgExGWldwv1AEzZm+Ki9/vGfyjkL33pbSr9HGpt2Xg= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -614,18 +622,20 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/scanoss/go-grpc-helper v0.11.0 h1:DifUX7KrQObTo9ta/vc4vqSzAdDEy1yNl+zWKuX5iOc= -github.com/scanoss/go-grpc-helper v0.11.0/go.mod h1:p2lhQTs6X5Y4E2F50qG6DbGpATtX/YYMycEcFwo9XVE= -github.com/scanoss/go-models v0.4.0 h1:TPAWgFzseChYe12RHVcsfdouZH8AleiPphKA7TwOd04= -github.com/scanoss/go-models v0.4.0/go.mod h1:Dq8ag9CI/3h0sqDWYUrTjW/jO8l5L6oopWJRKtJxzqA= -github.com/scanoss/go-purl-helper v0.2.1 h1:jp960a585ycyJSlqZky1NatMJBIQi/JGITDfNSu/9As= -github.com/scanoss/go-purl-helper v0.2.1/go.mod h1:v20/bKD8G+vGrILdiq6r0hyRD2bO8frCJlu9drEcQ38= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/scanoss/go-component-helper v0.5.0 h1:9t+vZIGKQYGj7Bomv3uKWe6bwS5yRrAOP5zJOTrN1Sw= +github.com/scanoss/go-component-helper v0.5.0/go.mod h1:8RZU7jPsdwuQTB68yQ06yhrlHglU3vhn348EIiFuKaw= +github.com/scanoss/go-grpc-helper v0.13.0 h1:GtKDKuc2jTtv27naIVF3f095tuJvSvxbeN+HmUdBS4Y= +github.com/scanoss/go-grpc-helper v0.13.0/go.mod h1:mNfM/788jzo8rQ2K2Y97UYQPd6MjuVXEM7r05kWPOJc= +github.com/scanoss/go-models v0.7.0 h1:6uhr3sGWIFz3wb1k+nhFfEoGbR8n5e5q3kJGJuLqdPc= +github.com/scanoss/go-models v0.7.0/go.mod h1:gkXP2/3ZUn8IyH/z/528aTcWZ2mzlzsXiY4bzBbrM+o= +github.com/scanoss/go-purl-helper v0.3.0 h1:zH5rcYbmYTvKms2oWrYV+8rWZ2ElLgDIOy2jZ9XhAg0= +github.com/scanoss/go-purl-helper v0.3.0/go.mod h1:3CFUM/OuUp9Q58IF/yGkQhr+G4x6hJNmF8N1f0W82C4= github.com/scanoss/ipfilter/v2 v2.0.2 h1:GaB9i8kVJg9JQZm5XGStYkEpiaCVdsrj7ezI2wV/oh8= github.com/scanoss/ipfilter/v2 v2.0.2/go.mod h1:AwrpX4XGbZ7EKISMi1d6E5csBk1nWB8+ugpvXHFcTpA= -github.com/scanoss/papi v0.28.0 h1:uvevFYoxwzvSH1hvgBoAkScIGTK2U1+rLzHSoJdnARk= -github.com/scanoss/papi v0.28.0/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= +github.com/scanoss/papi v0.33.0 h1:Qt7EABUapXzNlaFT2uIPIkpaHyCsLe2+PUPwn2iVkpA= +github.com/scanoss/papi v0.33.0/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= github.com/scanoss/zap-logging-helper v0.4.0 h1:2qTYoaFa9+MlD2/1wmPtiDHfh+42NIEwgKVU3rPpl0Y= github.com/scanoss/zap-logging-helper v0.4.0/go.mod h1:9QuEZcq73g/0Izv1tWeOWukoIK0oTBzM4jSNQ5kRR1w= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -664,30 +674,30 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -696,8 +706,8 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -717,8 +727,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -747,8 +755,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -800,8 +808,8 @@ golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -843,8 +851,8 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -915,8 +923,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -933,8 +941,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -997,8 +1005,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1180,10 +1188,10 @@ google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZV google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1222,8 +1230,8 @@ google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1240,8 +1248,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -1261,18 +1269,20 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= -modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= -modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= -modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= -modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= -modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1281,8 +1291,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= -modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/pkg/cmd/server.go b/pkg/cmd/server.go index e82992c..5d99425 100644 --- a/pkg/cmd/server.go +++ b/pkg/cmd/server.go @@ -21,8 +21,13 @@ import ( _ "embed" "flag" "fmt" + "net/http" + "os" + "strings" + "github.com/golobby/config/v3" "github.com/golobby/config/v3/pkg/feeder" + "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "github.com/scanoss/go-grpc-helper/pkg/files" gd "github.com/scanoss/go-grpc-helper/pkg/grpc/database" @@ -30,23 +35,22 @@ import ( gomodels "github.com/scanoss/go-models/pkg/models" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" _ "modernc.org/sqlite" - "net/http" - "os" myconfig "scanoss.com/components/pkg/config" "scanoss.com/components/pkg/protocol/grpc" "scanoss.com/components/pkg/protocol/rest" "scanoss.com/components/pkg/service" - "strings" ) -//TODO: Now the config includes the app version. +//TODO: Now the config includes the app version. // This might be worth moving to the file pkg/config/server_config.go //go:generate bash ../../get_version.sh //go:embed version.txt var version string -// getConfig checks command line args for option to feed into the config parser +// getConfig checks command line args for option to feed into the config parser. +// It performs a two-phase initialization: first loads basic config to get logging settings, +// then initializes the logger and reloads config with the proper logger for StatusMapper. func getConfig() (*myconfig.ServerConfig, error) { var jsonConfig, envConfig string flag.StringVar(&jsonConfig, "json-config", "", "Application JSON config") @@ -73,21 +77,26 @@ func getConfig() (*myconfig.ServerConfig, error) { } } myConfig, err := myconfig.NewServerConfig(feeders) + if err != nil { + return nil, err + } + // Initialize the application logger + err = zlog.SetupAppLogger(myConfig.App.Mode, myConfig.Logging.ConfigFile, myConfig.App.Debug) + if err != nil { + return nil, err + } + // Initialise the status mapping config + myConfig.InitStatusMapperConfig(zlog.S) return myConfig, err } -// RunServer runs the gRPC Component Server +// RunServer runs the gRPC Component Server. func RunServer() error { - // Load command line options and config + // Load command line options and config (logger is initialized inside getConfig) cfg, err := getConfig() if err != nil { return fmt.Errorf("failed to load config: %v", err) } - - err = zlog.SetupAppLogger(cfg.App.Mode, cfg.Logging.ConfigFile, cfg.App.Debug) - if err != nil { - return err - } defer zlog.SyncZap() // Check if TLS/SSL should be enabled startTLS, err := files.CheckTLS(cfg.TLS.CertFile, cfg.TLS.KeyFile) @@ -99,14 +108,12 @@ func RunServer() error { if err != nil { return err } - // Set the default version from the embedded binary version if not overridden by config/env if len(cfg.App.Version) == 0 { cfg.App.Version = strings.TrimSpace(version) } zlog.S.Infof("Starting SCANOSS Component Service: %v", cfg.App.Version) - - // Setup database connection pool + // Set up the database connection pool db, err := gd.OpenDBConnection(cfg.Database.Dsn, cfg.Database.Driver, cfg.Database.User, cfg.Database.Passwd, cfg.Database.Host, cfg.Database.Schema, cfg.Database.SslMode) if err != nil { @@ -117,17 +124,8 @@ func RunServer() error { } defer gd.CloseDBConnection(db) // Log database version info - dbVersionModel := gomodels.NewDBVersionModel(db) - dbVersion, dbVersionErr := dbVersionModel.GetCurrentVersion(context.Background()) - if dbVersionErr != nil { - zlog.S.Warnf("Could not read db_version table: %v", dbVersionErr) - } else if len(dbVersion.SchemaVersion) > 0 { - zlog.S.Infof("Loaded decoration DB: package=%s, schema=%s, created_at=%s", - dbVersion.PackageName, dbVersion.SchemaVersion, dbVersion.CreatedAt) - } else { - zlog.S.Warn("db_version table is empty") - } - // Setup dynamic logging (if necessary) + logDBVersion(db) + // Set up dynamic logging (if necessary) zlog.SetupAppDynamicLogging(cfg.Logging.DynamicPort, cfg.Logging.DynamicLogging) // Register the component service v2API := service.NewComponentServer(db, cfg) @@ -147,3 +145,19 @@ func RunServer() error { // graceful shutdown return gs.WaitServerComplete(srv, server) } + +// logDBVersion logs the current version of the database. +func logDBVersion(db *sqlx.DB) { + // Log database version info + dbVersionModel := gomodels.NewDBVersionModel(db) + dbVersion, dbVersionErr := dbVersionModel.GetCurrentVersion(context.Background()) + switch { + case dbVersionErr != nil: + zlog.S.Warnf("Could not read db_version table: %v", dbVersionErr) + case len(dbVersion.SchemaVersion) > 0: + zlog.S.Infof("Loaded decoration DB: package=%s, schema=%s, created_at=%s", + dbVersion.PackageName, dbVersion.SchemaVersion, dbVersion.CreatedAt) + default: + zlog.S.Warn("db_version table is empty") + } +} diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index 58c10ea..cda7cbf 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2018-2022 SCANOSS.COM + * Copyright (C) 2018-2026 SCANOSS.COM * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,8 +17,12 @@ package config import ( + "encoding/json" + "github.com/golobby/config/v3" "github.com/golobby/config/v3/pkg/feeder" + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" + "go.uber.org/zap" ) const ( @@ -26,7 +30,22 @@ const ( defaultRestPort = "40053" ) -// ServerConfig is configuration for Server +// parseStatusMappingString converts a string to interface{} for StatusMapper +// It handles both JSON object format (from config file) and JSON string format (from env var). +func parseStatusMappingString(s string) interface{} { + if s == "" { + return nil + } + // Try to unmarshal as map first (JSON object from config file) + var m map[string]interface{} + if err := json.Unmarshal([]byte(s), &m); err == nil { + return m + } + // Otherwise return as string (JSON string from env var) + return s +} + +// ServerConfig is a configuration for Server. type ServerConfig struct { App struct { Name string `env:"APP_NAME"` @@ -68,9 +87,14 @@ type ServerConfig struct { BlockByDefault bool `env:"COMP_BLOCK_BY_DEFAULT"` // Block request by default if they are not in the allow list TrustProxy bool `env:"COMP_TRUST_PROXY"` // Trust the interim proxy or not (causes the source IP to be validated instead of the proxy) } + StatusMapping struct { + Mapping string `env:"STATUS_MAPPING"` // JSON string mapping DB statuses to classified statuses (from env or file) + } + // StatusMapper is the compiled status mapper (initialised once at startup) + statusMapper *StatusMapper } -// NewServerConfig loads all config options and return a struct for use +// NewServerConfig loads all config options and return a struct for use. func NewServerConfig(feeders []config.Feeder) (*ServerConfig, error) { cfg := ServerConfig{} setServerConfigDefaults(&cfg) @@ -87,7 +111,7 @@ func NewServerConfig(feeders []config.Feeder) (*ServerConfig, error) { return &cfg, nil } -// setServerConfigDefaults attempts to set reasonable defaults for the server config +// setServerConfigDefaults attempts to set reasonable defaults for the server config. func setServerConfigDefaults(cfg *ServerConfig) { cfg.App.Name = "SCANOSS Component Server" cfg.App.GRPCPort = defaultGrpcPort @@ -106,3 +130,17 @@ func setServerConfigDefaults(cfg *ServerConfig) { cfg.Telemetry.Enabled = false cfg.Telemetry.OltpExporter = "0.0.0.0:4317" // Default OTEL OLTP gRPC Exporter endpoint } + +// InitStatusMapperConfig initialise the status mapper for mapping component statuses. +func (cfg *ServerConfig) InitStatusMapperConfig(s *zap.SugaredLogger) { + cfg.statusMapper = NewStatusMapper(s, parseStatusMappingString(cfg.StatusMapping.Mapping)) +} + +// GetStatusMapper returns the status mapper for mapping database statuses to classified statuses. +func (cfg *ServerConfig) GetStatusMapper() *StatusMapper { + // Initialise the mapper if it wasn't done previously + if cfg.statusMapper == nil { + cfg.InitStatusMapperConfig(zlog.S) + } + return cfg.statusMapper +} diff --git a/pkg/config/server_config_integration_test.go b/pkg/config/server_config_integration_test.go new file mode 100644 index 0000000..64db7b9 --- /dev/null +++ b/pkg/config/server_config_integration_test.go @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package config + +import ( + "fmt" + "os" + "testing" + + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" +) + +// TestServerConfig_StatusMapping_FromEnv verifies that custom status mappings can be loaded from environment variables. +// Tests that STATUS_MAPPING env var is correctly parsed as JSON and applied to the StatusMapper. +// Verifies both custom mappings and default fallback behavior for non-overridden keys. +func TestServerConfig_StatusMapping_FromEnv(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("Failed to initialise logger: %v", err) + } + defer zlog.SyncZap() + envValue := `{"unlisted":"custom-removed","yanked":"custom-yanked"}` + errEnv := os.Setenv("STATUS_MAPPING", envValue) + if errEnv != nil { + t.Fatalf("Could not set env variable: %v", errEnv) + } + cfg, err := NewServerConfig(nil) + if err != nil { + t.Fatalf("Failed to load config: %v", err) + } + if ue := os.Unsetenv("STATUS_MAPPING"); ue != nil { + fmt.Printf("Warning: Problem running Unsetenv: %v\n", ue) + } + // Allowing the GetStatusMapper to load the config + mapper := cfg.GetStatusMapper() + if mapper == nil { + t.Fatal("Expected non-nil mapper from GetStatusMapper()") + } + result := mapper.MapStatus("unlisted") + if result != "custom-removed" { + t.Errorf("Expected 'custom-removed', got %q", result) + } + result = mapper.MapStatus("yanked") + if result != "custom-yanked" { + t.Errorf("Expected 'custom-yanked', got %q", result) + } + result = mapper.MapStatus("deleted") + if result != "deleted" { + t.Errorf("Expected 'deleted', got %q", result) + } +} + +// TestServerConfig_StatusMapping_DefaultWhenNotSet verifies that default status mappings are used when STATUS_MAPPING is not configured. +// Tests that StatusMapper initializes correctly with built-in default mappings. +// Ensures default behavior when no custom configuration is provided. +func TestServerConfig_StatusMapping_DefaultWhenNotSet(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("Failed to initialise logger: %v", err) + } + defer zlog.SyncZap() + errEnv := os.Unsetenv("STATUS_MAPPING") + if errEnv != nil { + t.Fatalf("Could not set env variable: %v", errEnv) + } + cfg, err := NewServerConfig(nil) + if err != nil { + t.Fatalf("Failed to load config: %v", err) + } + mapper := cfg.GetStatusMapper() + if mapper == nil { + t.Fatal("Expected non-nil mapper from GetStatusMapper()") + } + result := mapper.MapStatus("unlisted") + if result != "removed" { + t.Errorf("Expected 'removed', got %q", result) + } + result = mapper.MapStatus("yanked") + if result != "removed" { + t.Errorf("Expected 'removed', got %q", result) + } + result = mapper.MapStatus("active") + if result != "active" { + t.Errorf("Expected 'active', got %q", result) + } +} + +// TestServerConfig_StatusMapping_WithProvidedLogger verifies that StatusMapper receives and uses an explicitly provided logger. +// Simulates production usage where logger is initialized before config loading (two-phase initialization). +// Tests that custom mappings work correctly when passing an initialized logger to NewServerConfig. +func TestServerConfig_StatusMapping_WithProvidedLogger(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("Failed to initialise logger: %v", err) + } + defer zlog.SyncZap() + envValue := `{"test-status":"test-mapped"}` + errEnv := os.Setenv("STATUS_MAPPING", envValue) + if errEnv != nil { + t.Fatalf("Could not set env variable: %v", errEnv) + } + cfg, err := NewServerConfig(nil) + if err != nil { + t.Fatalf("Failed to load config: %v", err) + } + if ue := os.Unsetenv("STATUS_MAPPING"); ue != nil { + fmt.Printf("Warning: Problem running Unsetenv: %v\n", ue) + } + + cfg.InitStatusMapperConfig(zlog.S) + mapper := cfg.GetStatusMapper() + if mapper == nil { + t.Fatal("Expected non-nil mapper from GetStatusMapper()") + } + result := mapper.MapStatus("test-status") + if result != "test-mapped" { + t.Errorf("Expected 'test-mapped', got %q", result) + } +} diff --git a/pkg/config/server_config_test.go b/pkg/config/server_config_test.go index 352a291..df45f6a 100644 --- a/pkg/config/server_config_test.go +++ b/pkg/config/server_config_test.go @@ -18,32 +18,42 @@ package config import ( "fmt" - "github.com/golobby/config/v3" - "github.com/golobby/config/v3/pkg/feeder" "os" "testing" + + "github.com/golobby/config/v3" + "github.com/golobby/config/v3/pkg/feeder" ) +// TestServerConfig verifies that NewServerConfig can load configuration from environment variables. +// It tests that environment variables are properly loaded and override default values. +// Uses nil logger parameter to test fallback to zap.S() behavior. func TestServerConfig(t *testing.T) { + // Set environment variable for database user dbUser := "test-user" err := os.Setenv("DB_USER", dbUser) if err != nil { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } + // Load config with nil feeders and nil logger (uses env vars and fallback logger) cfg, err := NewServerConfig(nil) if err != nil { t.Fatalf("an error '%s' was not expected when creating new config instance", err) } + // Verify environment variable was loaded correctly if cfg.Database.User != dbUser { t.Errorf("DB user '%v' doesn't match expected: %v", cfg.Database.User, dbUser) } fmt.Printf("Server Config1: %+v\n", cfg) + // Cleanup err = os.Unsetenv("DB_USER") if err != nil { fmt.Printf("Warning: Problem runn Unsetenv: %v\n", err) } } +// TestServerConfigDotEnv verifies that NewServerConfig can load configuration from a .env file. +// Tests the DotEnv feeder functionality and ensures file-based config takes precedence over defaults. func TestServerConfigDotEnv(t *testing.T) { err := os.Unsetenv("DB_USER") if err != nil { @@ -62,6 +72,8 @@ func TestServerConfigDotEnv(t *testing.T) { fmt.Printf("Server Config2: %+v\n", cfg) } +// TestServerConfigJson verifies that NewServerConfig can load configuration from a JSON file. +// Tests the Json feeder functionality and ensures JSON file-based config takes precedence over defaults. func TestServerConfigJson(t *testing.T) { err := os.Unsetenv("DB_USER") if err != nil { diff --git a/pkg/config/status_mapper.go b/pkg/config/status_mapper.go new file mode 100644 index 0000000..efd222a --- /dev/null +++ b/pkg/config/status_mapper.go @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2026 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package config + +import ( + "encoding/json" + "strings" + + "go.uber.org/zap" +) + +// StatusMapper handles mapping of database statuses to classified statuses. +type StatusMapper struct { + mapping map[string]string + s *zap.SugaredLogger +} + +// NewStatusMapper creates a new StatusMapper with the provided mapping +// mappingConfig can be: +// - map[string]interface{} (from JSON config file) +// - string (from environment variable, containing JSON) +// - nil or empty (uses default mappings) +func NewStatusMapper(s *zap.SugaredLogger, mappingConfig interface{}) *StatusMapper { + mapper := &StatusMapper{ + s: s, + mapping: getDefaultStatusMapping(), + } + if mappingConfig == nil { + return mapper + } + customMapping := parseMappingConfig(s, mappingConfig) + if customMapping != nil { + // Merge custom mapping with defaults (custom overrides defaults) + for key, value := range customMapping { + mapper.mapping[strings.ToLower(key)] = value + } + if s != nil { + s.Infof("Loaded custom status mapping with %d entries", len(customMapping)) + } + } + + return mapper +} + +// parseMappingConfig parses the mapping configuration from various formats. +func parseMappingConfig(s *zap.SugaredLogger, mappingConfig interface{}) map[string]string { + switch v := mappingConfig.(type) { + case string: + // String format (from environment variable) + return parseJSONString(s, v) + case map[string]interface{}: + // Map format (from JSON config file) + return convertInterfaceMap(s, v) + case map[string]string: + // Direct map format + return v + default: + if s != nil { + s.Warnf("Unexpected mapping config type: %T, using defaults", mappingConfig) + } + return nil + } +} + +// parseJSONString parses a JSON string into a map. +func parseJSONString(s *zap.SugaredLogger, jsonStr string) map[string]string { + if len(strings.TrimSpace(jsonStr)) == 0 { + return nil + } + var result map[string]string + err := json.Unmarshal([]byte(jsonStr), &result) + if err != nil { + if s != nil { + s.Warnf("Failed to parse STATUS_MAPPING JSON string, using defaults: %v", err) + } + return nil + } + return result +} + +// convertInterfaceMap converts map[string]interface{} to map[string]string. +func convertInterfaceMap(s *zap.SugaredLogger, m map[string]interface{}) map[string]string { + result := make(map[string]string, len(m)) + for key, value := range m { + if strValue, ok := value.(string); ok { + result[key] = strValue + } else if s != nil { + s.Warnf("Skipping non-string value for key %q: %v (type: %T)", key, value, value) + } + } + return result +} + +// MapStatus maps a database status to its classified status +// Returns the mapped status, or the original if no mapping exists. +func (m *StatusMapper) MapStatus(dbStatus string) string { + if dbStatus == "" { + return "" + } + // Normalise to lowercase for lookup + normalized := strings.ToLower(strings.TrimSpace(dbStatus)) + if mapped, exists := m.mapping[normalized]; exists { + return mapped + } + // If no mapping exists, return the original value + return dbStatus +} + +// getDefaultStatusMapping returns the default status classification mapping. +func getDefaultStatusMapping() map[string]string { + return map[string]string{ + "active": "active", + "unlisted": "removed", + "yanked": "removed", + "deleted": "deleted", + "deprecated": "deprecated", + "unpublished": "removed", + "archived": "deprecated", + } +} diff --git a/pkg/config/status_mapper_test.go b/pkg/config/status_mapper_test.go new file mode 100644 index 0000000..cb7508f --- /dev/null +++ b/pkg/config/status_mapper_test.go @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2026 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package config + +import ( + "testing" + + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" +) + +const removedStatus = "removed" +const activeStatus = "active" + +func TestStatusMapper_MapStatus_DefaultMappings(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with empty JSON (should use defaults) + mapper := NewStatusMapper(s, "") + + testCases := []struct { + name string + input string + expected string + }{ + {"active maps to active", activeStatus, activeStatus}, + {"unlisted maps to removed", "unlisted", removedStatus}, + {"yanked maps to removed", "yanked", removedStatus}, + {"deleted maps to deleted", "deleted", "deleted"}, + {"deprecated maps to deprecated", "deprecated", "deprecated"}, + {"unpublished maps to removed", "unpublished", removedStatus}, + {"archived maps to deprecated", "archived", "deprecated"}, + {"ACTIVE (uppercase) maps to active", "ACTIVE", "active"}, + {"Unlisted (mixed case) maps to removed", "Unlisted", removedStatus}, + {"unknown status returns original", "unknown-status", "unknown-status"}, + {"empty string returns empty", "", ""}, + {"whitespace status returns original", " some status ", " some status "}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := mapper.MapStatus(tc.input) + if result != tc.expected { + t.Errorf("MapStatus(%q) = %q, expected %q", tc.input, result, tc.expected) + } + }) + } +} + +func TestStatusMapper_MapStatus_CustomMappings(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with custom JSON mappings + customJSON := `{"unlisted":"custom-removed","new-status":"custom-value","active":"still-active"}` + mapper := NewStatusMapper(s, customJSON) + + testCases := []struct { + name string + input string + expected string + }{ + {"custom unlisted mapping", "unlisted", "custom-removed"}, + {"custom new-status mapping", "new-status", "custom-value"}, + {"custom active override", activeStatus, "still-active"}, + {"default yanked still works", "yanked", "removed"}, + {"default deleted still works", "deleted", "deleted"}, + {"unknown status returns original", "completely-unknown", "completely-unknown"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := mapper.MapStatus(tc.input) + if result != tc.expected { + t.Errorf("MapStatus(%q) = %q, expected %q", tc.input, result, tc.expected) + } + }) + } +} + +func TestStatusMapper_MapStatus_InvalidJSON(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with invalid JSON (should fall back to defaults) + invalidJSON := `{this is not valid json}` + mapper := NewStatusMapper(s, invalidJSON) + + // Should use defaults when JSON is invalid + result := mapper.MapStatus("unlisted") + if result != removedStatus { + t.Errorf("MapStatus with invalid JSON should use defaults, got %q, expected %q", result, removedStatus) + } + + result = mapper.MapStatus(activeStatus) + if result != activeStatus { + t.Errorf("MapStatus with invalid JSON should use defaults, got %q, expected %q", result, activeStatus) + } +} + +func TestStatusMapper_MapStatus_EmptyJSON(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Test various empty JSON scenarios + emptyScenarios := []string{"", " ", "{}", " {} "} + + for _, emptyJSON := range emptyScenarios { + mapper := NewStatusMapper(s, emptyJSON) + + // Should use defaults + result := mapper.MapStatus("unlisted") + if result != removedStatus { + t.Errorf("MapStatus with empty JSON %q should use defaults, got %q, expected %q", emptyJSON, result, removedStatus) + } + } +} + +func TestStatusMapper_MapStatus_CaseSensitivity(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + mapper := NewStatusMapper(s, "") + + // Test that mapping is case-insensitive for lookup + testCases := []struct { + input string + expected string + }{ + {activeStatus, activeStatus}, + {"ACTIVE", activeStatus}, + {"Active", activeStatus}, + {"AcTiVe", activeStatus}, + {"unlisted", removedStatus}, + {"UNLISTED", removedStatus}, + {"Unlisted", removedStatus}, + {"UnLiStEd", removedStatus}, + } + + for _, tc := range testCases { + result := mapper.MapStatus(tc.input) + if result != tc.expected { + t.Errorf("MapStatus(%q) = %q, expected %q (case-insensitive)", tc.input, result, tc.expected) + } + } +} + +func TestGetDefaultStatusMapping(t *testing.T) { + defaults := getDefaultStatusMapping() + + expectedMappings := map[string]string{ + activeStatus: activeStatus, + "unlisted": removedStatus, + "yanked": removedStatus, + "deleted": "deleted", + "deprecated": "deprecated", + "unpublished": removedStatus, + "archived": "deprecated", + } + + if len(defaults) != len(expectedMappings) { + t.Errorf("Expected %d default mappings, got %d", len(expectedMappings), len(defaults)) + } + + for key, expectedValue := range expectedMappings { + if actualValue, exists := defaults[key]; !exists { + t.Errorf("Default mapping missing key %q", key) + } else if actualValue != expectedValue { + t.Errorf("Default mapping for %q: got %q, expected %q", key, actualValue, expectedValue) + } + } +} + +func TestStatusMapper_MapStatus_MapFormat(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with map[string]interface{} format (as from JSON config file) + customMapping := map[string]interface{}{ + "unlisted": "custom-removed", + "new-status": "custom-value", + activeStatus: "still-active", + } + mapper := NewStatusMapper(s, customMapping) + + testCases := []struct { + name string + input string + expected string + }{ + {"map format: custom unlisted mapping", "unlisted", "custom-removed"}, + {"map format: custom new-status mapping", "new-status", "custom-value"}, + {"map format: custom active override", activeStatus, "still-active"}, + {"map format: default yanked still works", "yanked", removedStatus}, + {"map format: default deleted still works", "deleted", "deleted"}, + {"map format: unknown status returns original", "completely-unknown", "completely-unknown"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := mapper.MapStatus(tc.input) + if result != tc.expected { + t.Errorf("MapStatus(%q) = %q, expected %q", tc.input, result, tc.expected) + } + }) + } +} + +func TestStatusMapper_NilConfig(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with nil config (should use defaults) + mapper := NewStatusMapper(s, nil) + + result := mapper.MapStatus("unlisted") + if result != removedStatus { + t.Errorf("MapStatus with nil config should use defaults, got %q, expected %q", result, removedStatus) + } +} + +func TestStatusMapper_MapWithNonStringValue(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := zlog.S + + // Create mapper with map containing non-string values + customMapping := map[string]interface{}{ + "unlisted": "custom-removed", + activeStatus: 123, // Invalid: not a string + } + mapper := NewStatusMapper(s, customMapping) + + // unlisted should work (it's a string) + result := mapper.MapStatus("unlisted") + if result != "custom-removed" { + t.Errorf("MapStatus('unlisted') = %q, expected %q", result, "custom-removed") + } + + // active should use default (non-string value was skipped) + result = mapper.MapStatus(activeStatus) + if result != activeStatus { + t.Errorf("MapStatus('active') with non-string value should use default, got %q, expected %q", result, activeStatus) + } +} diff --git a/pkg/dtos/component_status_input.go b/pkg/dtos/component_status_input.go new file mode 100644 index 0000000..59e5af7 --- /dev/null +++ b/pkg/dtos/component_status_input.go @@ -0,0 +1,63 @@ +package dtos + +import ( + "encoding/json" + "errors" + "fmt" + + "go.uber.org/zap" +) + +// ComponentStatusInput represents a single component status request. +type ComponentStatusInput struct { + Purl string `json:"purl"` + Requirement string `json:"requirement,omitempty"` +} + +// ComponentsStatusInput represents a request for multiple component statuses. +type ComponentsStatusInput struct { + Components []ComponentStatusInput `json:"components"` +} + +// ParseComponentStatusInput unmarshals JSON bytes into a ComponentStatusInput struct. +// +// Parameters: +// - s: Sugared logger for error logging +// - input: JSON byte array to be unmarshaled +// +// Returns: +// - ComponentStatusInput struct populated from JSON, or error if unmarshaling fails or input is empty +func ParseComponentStatusInput(s *zap.SugaredLogger, input []byte) (ComponentStatusInput, error) { + if len(input) == 0 { + return ComponentStatusInput{}, errors.New("no data supplied to parse") + } + var data ComponentStatusInput + err := json.Unmarshal(input, &data) + if err != nil { + s.Errorf("Parse failure: %v", err) + return ComponentStatusInput{}, fmt.Errorf("failed to parse data: %v", err) + } + return data, nil +} + +// ParseComponentsStatusInput unmarshals JSON bytes into a ComponentsStatusInput struct. +// Used for parsing batch component status requests containing multiple components. +// +// Parameters: +// - s: Sugared logger for error logging +// - input: JSON byte array to be unmarshaled +// +// Returns: +// - ComponentsStatusInput struct with array of component status requests, or error if unmarshaling fails or input is empty +func ParseComponentsStatusInput(s *zap.SugaredLogger, input []byte) (ComponentsStatusInput, error) { + if len(input) == 0 { + return ComponentsStatusInput{}, errors.New("no data supplied to parse") + } + var data ComponentsStatusInput + err := json.Unmarshal(input, &data) + if err != nil { + s.Errorf("Parse failure: %v", err) + return ComponentsStatusInput{}, fmt.Errorf("failed to parse data: %v", err) + } + return data, nil +} diff --git a/pkg/dtos/component_status_output.go b/pkg/dtos/component_status_output.go new file mode 100644 index 0000000..ff06152 --- /dev/null +++ b/pkg/dtos/component_status_output.go @@ -0,0 +1,47 @@ +package dtos + +import ( + "github.com/scanoss/go-grpc-helper/pkg/grpc/domain" +) + +// ComponentStatusOutput represents the status information for a single component. +type ComponentStatusOutput struct { + Purl string `json:"purl"` + Name string `json:"name"` + Requirement string `json:"requirement,omitempty"` + VersionStatus *VersionStatusOutput `json:"version_status,omitempty"` + ComponentStatus *ComponentStatusInfo `json:"component_status,omitempty"` +} + +// VersionStatusOutput represents the status of a specific version. +type VersionStatusOutput struct { + Version string `json:"version"` + Status string `json:"status"` + RepositoryStatus string `json:"repository_status,omitempty"` + IndexedDate string `json:"indexed_date,omitempty"` + StatusChangeDate string `json:"status_change_date,omitempty"` + ErrorMessage *string `json:"error_message,omitempty"` + ErrorCode *domain.StatusCode `json:"error_code,omitempty"` +} + +// ComponentStatusInfo represents the status of a component (ignoring version). +type ComponentStatusInfo struct { + Status string `json:"status"` + RepositoryStatus string `json:"repository_status,omitempty"` + FirstIndexedDate string `json:"first_indexed_date,omitempty"` + LastIndexedDate string `json:"last_indexed_date,omitempty"` + StatusChangeDate string `json:"status_change_date,omitempty"` + ErrorMessage *string `json:"error_message,omitempty"` + ErrorCode *domain.StatusCode `json:"error_code,omitempty"` +} + +// ComponentsStatusOutput represents the status information for multiple components. +type ComponentsStatusOutput struct { + Components []ComponentStatusOutput `json:"components"` +} + +// StringPtr returns a pointer to the provided string value +// This is useful for optional string fields that require pointers. +func StringPtr(s string) *string { + return &s +} diff --git a/pkg/dtos/component_version_input.go b/pkg/dtos/component_version_input.go index 2bd1b82..07b071d 100644 --- a/pkg/dtos/component_version_input.go +++ b/pkg/dtos/component_version_input.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "go.uber.org/zap" ) diff --git a/pkg/dtos/component_version_input_test.go b/pkg/dtos/component_version_input_test.go index 49185d2..d856390 100644 --- a/pkg/dtos/component_version_input_test.go +++ b/pkg/dtos/component_version_input_test.go @@ -3,10 +3,11 @@ package dtos import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - "testing" ) func TestParseComponentVersionsInput(t *testing.T) { @@ -96,5 +97,4 @@ func TestExportComponentVersionsInput(t *testing.T) { t.Errorf("Failed to export component version input: %v\n", err) } fmt.Printf("Converting empty component version input json to bytes: %v\n", bytes) - } diff --git a/pkg/dtos/component_version_output.go b/pkg/dtos/component_version_output.go index 0b69d88..13a2835 100644 --- a/pkg/dtos/component_version_output.go +++ b/pkg/dtos/component_version_output.go @@ -17,7 +17,7 @@ type ComponentOutput struct { Component string `json:"component"` // Deprecated. Component and name fields will contain the same data until // the component field is removed Purl string `json:"purl"` - Url string `json:"url"` + URL string `json:"url"` Versions []ComponentVersion `json:"versions"` } @@ -29,7 +29,7 @@ type ComponentVersion struct { type ComponentLicense struct { Name string `json:"name"` - SpdxId string `json:"spdx_id"` + SpdxID string `json:"spdx_id"` IsSpdx bool `json:"is_spdx_approved"` } diff --git a/pkg/dtos/component_version_output_test.go b/pkg/dtos/component_version_output_test.go index 6c84a55..2d05c70 100644 --- a/pkg/dtos/component_version_output_test.go +++ b/pkg/dtos/component_version_output_test.go @@ -3,12 +3,14 @@ package dtos import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - "testing" ) +//goland:noinspection DuplicatedCode func TestParseComponentVersionsOutput(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -35,14 +37,14 @@ func TestParseComponentVersionsOutput(t *testing.T) { want: ComponentVersionsOutput{Component: ComponentOutput{ Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", Versions: []ComponentVersion{ { Version: "1.8.3", Licenses: []ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -52,7 +54,7 @@ func TestParseComponentVersionsOutput(t *testing.T) { Licenses: []ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -91,9 +93,7 @@ func TestParseComponentVersionsOutput(t *testing.T) { if err == nil { t.Errorf("Expected an error for empty input") } - } - func TestExportComponentVersionsOutput(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -106,14 +106,14 @@ func TestExportComponentVersionsOutput(t *testing.T) { fullComponent := ComponentVersionsOutput{Component: ComponentOutput{ Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", Versions: []ComponentVersion{ { Version: "1.8.3", Licenses: []ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -123,7 +123,7 @@ func TestExportComponentVersionsOutput(t *testing.T) { Licenses: []ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -135,12 +135,11 @@ func TestExportComponentVersionsOutput(t *testing.T) { if err != nil { t.Errorf("dtos.ExportComponentVersionsOutput() error = %v", err) } - fmt.Println("Exported output data: ", data) + fmt.Println("Exported output data: ", string(data)) data, err = ExportComponentVersionsOutput(s, ComponentVersionsOutput{}) if err != nil { t.Errorf("dtos.ExportComponentVersionsOutput() error = %v", err) } - fmt.Println("Exported output data: ", data) - + fmt.Println("Exported output data: ", string(data)) } diff --git a/pkg/dtos/docs.go b/pkg/dtos/docs.go new file mode 100644 index 0000000..c50677c --- /dev/null +++ b/pkg/dtos/docs.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Package dtos contains data transfer objects (DTOs) for the Component service. +package dtos diff --git a/pkg/dtos/search_component_input.go b/pkg/dtos/search_component_input.go index 3d8b6ff..86724c0 100644 --- a/pkg/dtos/search_component_input.go +++ b/pkg/dtos/search_component_input.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "go.uber.org/zap" ) diff --git a/pkg/dtos/search_component_input_test.go b/pkg/dtos/search_component_input_test.go index 5d1d98a..e07a321 100644 --- a/pkg/dtos/search_component_input_test.go +++ b/pkg/dtos/search_component_input_test.go @@ -3,10 +3,11 @@ package dtos import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - "testing" ) func TestParseComponentSearchInput(t *testing.T) { diff --git a/pkg/dtos/search_component_output.go b/pkg/dtos/search_component_output.go index b06526b..c21f22e 100644 --- a/pkg/dtos/search_component_output.go +++ b/pkg/dtos/search_component_output.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "go.uber.org/zap" ) @@ -12,11 +13,10 @@ type ComponentsSearchOutput struct { } type ComponentSearchOutput struct { - Name string `json:"name"` - Component string `json:"component"` // Deprecated. Component and name fields will contain the same data until - // the component field is removed - Purl string `json:"purl"` - Url string `json:"url"` + Name string `json:"name"` // Deprecated. Component and name fields will contain the same data until + Component string `json:"component"` // the component field is removed + Purl string `json:"purl"` + URL string `json:"url"` } func ExportComponentSearchOutput(s *zap.SugaredLogger, output ComponentsSearchOutput) ([]byte, error) { diff --git a/pkg/dtos/search_component_output_test.go b/pkg/dtos/search_component_output_test.go index fd5d259..311141f 100644 --- a/pkg/dtos/search_component_output_test.go +++ b/pkg/dtos/search_component_output_test.go @@ -3,12 +3,14 @@ package dtos import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - "testing" ) +//goland:noinspection DuplicatedCode func TestParseComponentSearchOutput(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -28,7 +30,7 @@ func TestParseComponentSearchOutput(t *testing.T) { { Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", }, }}, }, @@ -86,7 +88,7 @@ func TestExportComponentSearchOutput(t *testing.T) { { Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", }, }} @@ -94,12 +96,11 @@ func TestExportComponentSearchOutput(t *testing.T) { if err != nil { t.Errorf("ExportComponentSearchOutput() error = %v", err) } - fmt.Println("Exported output data: ", data) + fmt.Println("Exported output data: ", string(data)) data, err = ExportComponentSearchOutput(s, ComponentsSearchOutput{}) if err != nil { t.Errorf("ExportComponentSearchOutput() error = %v", err) } - fmt.Println("Exported output data: ", data) - + fmt.Println("Exported output data: ", string(data)) } diff --git a/pkg/errors/error.go b/pkg/errors/error.go index 933478e..a9bf0b6 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +// Package errors contains error handling logic for the Component service. package errors import ( diff --git a/pkg/models/all_urls.go b/pkg/models/all_urls.go index 67eacf3..6029720 100644 --- a/pkg/models/all_urls.go +++ b/pkg/models/all_urls.go @@ -27,29 +27,29 @@ import ( "go.uber.org/zap" ) -type AllUrlsModel struct { +type AllURLsModel struct { ctx context.Context s *zap.SugaredLogger q *database.DBQueryContext } -type AllUrl struct { +type AllURL struct { Version string `db:"version"` Component string `db:"component"` License string `db:"license"` - LicenseId string `db:"license_id"` + LicenseID string `db:"license_id"` IsSpdx bool `db:"is_spdx"` PurlName string `db:"purl_name"` - MineId int32 `db:"mine_id"` + MineID int32 `db:"mine_id"` Date sql.NullString `db:"date"` - Url string `db:"-"` + URL string `db:"-"` } -func NewAllUrlModel(ctx context.Context, s *zap.SugaredLogger, q *database.DBQueryContext) *AllUrlsModel { - return &AllUrlsModel{ctx: ctx, s: s, q: q} +func NewAllURLModel(ctx context.Context, s *zap.SugaredLogger, q *database.DBQueryContext) *AllURLsModel { + return &AllURLsModel{ctx: ctx, s: s, q: q} } -func (m *AllUrlsModel) GetUrlsByPurlString(purlString string, limit int) ([]AllUrl, error) { +func (m *AllURLsModel) GetUrlsByPurlString(purlString string, limit int) ([]AllURL, error) { if len(purlString) == 0 { m.s.Errorf("Please specify a valid Purl String to query") return nil, errors.New("please specify a valid Purl String to query") @@ -65,7 +65,7 @@ func (m *AllUrlsModel) GetUrlsByPurlString(purlString string, limit int) ([]AllU return m.GetUrlsByPurlNameType(purlName, purl.Type, limit) } -func (m *AllUrlsModel) GetUrlsByPurlNameType(purlName, purlType string, limit int) ([]AllUrl, error) { +func (m *AllURLsModel) GetUrlsByPurlNameType(purlName, purlType string, limit int) ([]AllURL, error) { if len(purlName) == 0 { m.s.Errorf("Please specify a valid Purl Name to query") return nil, errors.New("please specify a valid Purl Name to query") @@ -79,7 +79,7 @@ func (m *AllUrlsModel) GetUrlsByPurlNameType(purlName, purlType string, limit in limit = defaultMaxVersionLimit } - var allUrls []AllUrl + var allUrls []AllURL err := m.q.SelectContext(m.ctx, &allUrls, ` SELECT DISTINCT diff --git a/pkg/models/all_urls_test.go b/pkg/models/all_urls_test.go index 29cf3f7..bed373f 100644 --- a/pkg/models/all_urls_test.go +++ b/pkg/models/all_urls_test.go @@ -18,16 +18,17 @@ package models import ( "context" + "testing" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/jmoiron/sqlx" "github.com/scanoss/go-grpc-helper/pkg/grpc/database" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" myconfig "scanoss.com/components/pkg/config" - "testing" ) -// setupTest initializes all necessary components for testing -func setupTest(t *testing.T) (*sqlx.DB, *sqlx.Conn, *AllUrlsModel) { +// setupTest initialises all necessary components for testing. +func setupTest(t *testing.T) (*sqlx.DB, *sqlx.Conn, *AllURLsModel) { err := zlog.NewSugaredDevLogger() if err != nil { t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) @@ -47,17 +48,17 @@ func setupTest(t *testing.T) (*sqlx.DB, *sqlx.Conn, *AllUrlsModel) { } myConfig.Database.Trace = true - return db, conn, NewAllUrlModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) + return db, conn, NewAllURLModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) } -// cleanup handles proper resource cleanup +// cleanup handles proper resource cleanup. func cleanup(db *sqlx.DB, conn *sqlx.Conn) { CloseConn(conn) CloseDB(db) zlog.SyncZap() } -// TestGetUrlsByPurlNameType tests the GetUrlsByPurlNameType function +// TestGetUrlsByPurlNameType tests the GetUrlsByPurlNameType function. func TestGetUrlsByPurlNameType(t *testing.T) { db, conn, allUrlsModel := setupTest(t) defer cleanup(db, conn) @@ -69,7 +70,7 @@ func TestGetUrlsByPurlNameType(t *testing.T) { limit int wantErr bool wantEmpty bool - validate func(t *testing.T, urls []AllUrl) + validate func(t *testing.T, urls []AllURL) }{ { name: "valid url search", @@ -78,7 +79,7 @@ func TestGetUrlsByPurlNameType(t *testing.T) { limit: -1, wantErr: false, wantEmpty: false, - validate: func(t *testing.T, urls []AllUrl) { + validate: func(t *testing.T, urls []AllURL) { if urls[0].PurlName != "tablestyle" { t.Errorf("expected purlName 'tablestyle', got %s", urls[0].PurlName) } @@ -91,9 +92,9 @@ func TestGetUrlsByPurlNameType(t *testing.T) { limit: 200, wantErr: false, wantEmpty: false, - validate: func(t *testing.T, urls []AllUrl) { + validate: func(t *testing.T, urls []AllURL) { // Filter URLs for specific version - var matchedUrls []AllUrl + var matchedUrls []AllURL for _, url := range urls { if url.PurlName == "grpcio" && url.Version == "1.12.1" { matchedUrls = append(matchedUrls, url) @@ -181,7 +182,7 @@ func TestGetUrlsByPurlNameType(t *testing.T) { } } -// TestGetUrlsByPurlString tests the GetUrlsByPurlString function +// TestGetUrlsByPurlString tests the GetUrlsByPurlString function. func TestGetUrlsByPurlString(t *testing.T) { db, conn, allUrlsModel := setupTest(t) defer cleanup(db, conn) @@ -192,7 +193,7 @@ func TestGetUrlsByPurlString(t *testing.T) { limit int wantErr bool wantEmpty bool - validate func(t *testing.T, urls []AllUrl) + validate func(t *testing.T, urls []AllURL) }{ { name: "valid purl", @@ -200,7 +201,7 @@ func TestGetUrlsByPurlString(t *testing.T) { limit: -1, wantErr: false, wantEmpty: false, - validate: func(t *testing.T, urls []AllUrl) { + validate: func(t *testing.T, urls []AllURL) { if urls[0].PurlName != "tablestyle" { t.Errorf("expected purlName 'tablestyle', got %s", urls[0].PurlName) } diff --git a/pkg/models/common.go b/pkg/models/common.go index ddd2260..b277b9e 100644 --- a/pkg/models/common.go +++ b/pkg/models/common.go @@ -21,16 +21,16 @@ package models import ( "context" "fmt" - "github.com/scanoss/go-grpc-helper/pkg/grpc/database" "os" "testing" "github.com/jmoiron/sqlx" + "github.com/scanoss/go-grpc-helper/pkg/grpc/database" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" ) -// loadSqlData Load the specified SQL files into the supplied DB -func loadSqlData(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, filename string) error { +// loadSQLData Load the specified SQL files into the supplied DB. +func loadSQLData(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, filename string) error { fmt.Printf("Loading test data file: %v\n", filename) file, err := os.ReadFile(filename) if err != nil { @@ -47,17 +47,17 @@ func loadSqlData(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, filename str return nil } -// LoadTestSQLData loads all the required test SQL files +// LoadTestSQLData loads all the required test SQL files. func LoadTestSQLData(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn) error { files := []string{"../models/tests/mines.sql", "../models/tests/all_urls.sql", "../models/tests/projects.sql", "../models/tests/licenses.sql", "../models/tests/versions.sql"} - return loadTestSqlDataFiles(db, ctx, conn, files) + return loadTestSQLDataFiles(db, ctx, conn, files) } -// loadTestSqlDataFiles loads a list of test SQL files -func loadTestSqlDataFiles(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, files []string) error { +// loadTestSQLDataFiles loads a list of test SQL files. +func loadTestSQLDataFiles(db *sqlx.DB, ctx context.Context, conn *sqlx.Conn, files []string) error { for _, file := range files { - err := loadSqlData(db, ctx, conn, file) + err := loadSQLData(db, ctx, conn, file) if err != nil { return err } @@ -109,13 +109,13 @@ type QueryJob struct { } type job struct { - jobId int + jobID int query string args []any } type result[T any] struct { - jobId int + jobID int query string err error dest []T @@ -126,7 +126,7 @@ func workerQuery[T any](q *database.DBQueryContext, ctx context.Context, jobs ch for j := range jobs { err := q.SelectContext(ctx, &structResults, j.query, j.args...) results <- result[T]{ - jobId: j.jobId, + jobID: j.jobID, query: j.query, err: err, dest: structResults, @@ -145,7 +145,7 @@ func RunQueries[T any](q *database.DBQueryContext, ctx context.Context, queryJob for i, queryJob := range queryJobs { jobChan <- job{ - jobId: i, + jobID: i, query: queryJob.Query, args: queryJob.Args, } @@ -156,7 +156,7 @@ func RunQueries[T any](q *database.DBQueryContext, ctx context.Context, queryJob for i := 0; i < numJobs; i++ { res := <-resultChan if res.err == nil { - resMap[res.jobId] = res.dest + resMap[res.jobID] = res.dest } else { return []T{}, res.err } diff --git a/pkg/models/common_test.go b/pkg/models/common_test.go index 11a81ca..fe92c1f 100644 --- a/pkg/models/common_test.go +++ b/pkg/models/common_test.go @@ -19,6 +19,8 @@ package models import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/jmoiron/sqlx" @@ -26,7 +28,6 @@ import ( zlog "github.com/scanoss/zap-logging-helper/pkg/logger" _ "modernc.org/sqlite" myconfig "scanoss.com/components/pkg/config" - "testing" ) func TestDbLoad(t *testing.T) { @@ -41,7 +42,7 @@ func TestDbLoad(t *testing.T) { t.Errorf("an error '%s' was not expected when opening a sugared logger", err) } defer CloseDB(db) - err = loadSqlData(db, nil, nil, "./tests/mines.sql") + err = loadSQLData(db, nil, nil, "./tests/mines.sql") if err != nil { t.Errorf("failed to load SQL test data: %v", err) } @@ -49,22 +50,21 @@ func TestDbLoad(t *testing.T) { if err != nil { t.Errorf("failed to load SQL test data: %v", err) } - err = loadSqlData(db, nil, nil, "./tests/does-not-exist.sql") + err = loadSQLData(db, nil, nil, "./tests/does-not-exist.sql") if err == nil { t.Errorf("did not fail to load SQL test data") } - err = loadTestSqlDataFiles(db, nil, nil, []string{"./tests/does-not-exist.sql"}) + err = loadTestSQLDataFiles(db, nil, nil, []string{"./tests/does-not-exist.sql"}) if err == nil { t.Errorf("did not fail to load SQL test data") } - err = loadSqlData(db, nil, nil, "./tests/bad_sql.sql") + err = loadSQLData(db, nil, nil, "./tests/bad_sql.sql") if err == nil { t.Errorf("did not fail to load SQL test data") } } func TestRunQueriesInParallel(t *testing.T) { - err := zlog.NewSugaredDevLogger() if err != nil { t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) @@ -74,8 +74,6 @@ func TestRunQueriesInParallel(t *testing.T) { s := ctxzap.Extract(ctx).Sugar() db := sqliteSetup(t) // Setup SQL Lite DB defer CloseDB(db) - //conn := sqliteConn(t, ctx, db) // Get a connection from the pool - //defer CloseConn(conn) err = LoadTestSQLData(db, ctx, nil) if err != nil { t.Fatalf("failed to load SQL test data: %v", err) @@ -101,11 +99,9 @@ func TestRunQueriesInParallel(t *testing.T) { t.Errorf("Error running multiple queries %v", err) } fmt.Printf("Result of running queries %v:\n%v\n ", queryJobs, res) - } func TestRemoveDuplicates(t *testing.T) { - err := zlog.NewSugaredDevLogger() if err != nil { t.Errorf("an error '%s' was not expected when opening a sugared logger", err) @@ -116,28 +112,28 @@ func TestRemoveDuplicates(t *testing.T) { Component: "hyx-decrypt", PurlType: "npm", PurlName: "hyx-decrypt", - Url: "", + URL: "", } comp1 := Component{ Component: "scanner", PurlType: "npm", PurlName: "scanner", - Url: "https://www.npmjs.com/package/scanner", + URL: "https://www.npmjs.com/package/scanner", } comp1Similar := Component{ Component: "scanner", PurlType: "npm", PurlName: "scanner", - Url: "www.npmjs.com/package/scanner", + URL: "www.npmjs.com/package/scanner", } comp2 := Component{ Component: "graph", PurlType: "npm", PurlName: "graph", - Url: "https://www.npmjs.com/package/graph", + URL: "https://www.npmjs.com/package/graph", } testTable := []struct { diff --git a/pkg/models/component_status.go b/pkg/models/component_status.go new file mode 100644 index 0000000..c655970 --- /dev/null +++ b/pkg/models/component_status.go @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2026 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package models + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/scanoss/go-grpc-helper/pkg/grpc/database" + purlhelper "github.com/scanoss/go-purl-helper/pkg" + "go.uber.org/zap" +) + +type ComponentStatusModel struct { + ctx context.Context + s *zap.SugaredLogger + q *database.DBQueryContext +} + +// ComponentVersionStatus represents the status information for a specific version. +type ComponentVersionStatus struct { + PurlName string `db:"purl_name"` + Version string `db:"version"` + IndexedDate sql.NullString `db:"indexed_date"` + VersionStatus sql.NullString `db:"version_status"` + VersionStatusChangeDate sql.NullString `db:"version_status_change_date"` +} + +// ComponentProjectStatus represents the status information for a component (ignoring version). +type ComponentProjectStatus struct { + PurlName string `db:"purl_name"` + Component string `db:"component"` + FirstIndexedDate sql.NullString `db:"first_indexed_date"` + LastIndexedDate sql.NullString `db:"last_indexed_date"` + Status sql.NullString `db:"status"` + StatusChangeDate sql.NullString `db:"status_change_date"` +} + +// ComponentFullStatus combines version and project status information. +type ComponentFullStatus struct { + ComponentVersionStatus + ComponentProjectStatus +} + +func NewComponentStatusModel(ctx context.Context, s *zap.SugaredLogger, q *database.DBQueryContext) *ComponentStatusModel { + return &ComponentStatusModel{ctx: ctx, s: s, q: q} +} + +// GetComponentStatusByPurlAndVersion gets status information for a specific component version. +func (m *ComponentStatusModel) GetComponentStatusByPurlAndVersion(purlString, version string) (*ComponentVersionStatus, error) { + if len(purlString) == 0 { + m.s.Errorf("Please specify a valid Purl String to query") + return nil, errors.New("please specify a valid Purl String to query") + } + purl, err := purlhelper.PurlFromString(purlString) + if err != nil { + return nil, err + } + purlName, err := purlhelper.PurlNameFromString(purlString) + if err != nil { + return nil, err + } + var status ComponentVersionStatus + // Query to get both version and component status + query := ` + SELECT DISTINCT au.purl_name, au."version", au.indexed_date, au.version_status, au.version_status_change_date + FROM + all_urls au, + mines m + WHERE + au.mine_id = m.id AND au.purl_name = $1 AND m.purl_type = $2 AND au."version" = $3 + ` + var results []ComponentVersionStatus + err = m.q.SelectContext(m.ctx, &results, query, purlName, purl.Type, version) + if err != nil { + m.s.Errorf("Failed to query component status for %v version %v: %v", purlName, version, err) + return nil, fmt.Errorf("failed to query component status: %v", err) + } + if len(results) == 0 { + m.s.Warnf("No status found for %v version %v", purlName, version) + return nil, fmt.Errorf("component version not found") + } + status = results[0] + m.s.Debugf("Found status for %v version %v", purlName, version) + return &status, nil +} + +// GetComponentStatusByPurl gets status information for the latest version of a component. +func (m *ComponentStatusModel) GetComponentStatusByPurl(purlString string) (*ComponentProjectStatus, error) { + if len(purlString) == 0 { + m.s.Errorf("Please specify a valid Purl String to query") + return nil, errors.New("please specify a valid Purl String to query") + } + purl, err := purlhelper.PurlFromString(purlString) + if err != nil { + return nil, err + } + purlName, err := purlhelper.PurlNameFromString(purlString) + if err != nil { + return nil, err + } + var status ComponentProjectStatus + // Query to get both version and component status for the latest version + query := ` + SELECT DISTINCT + p.component, + p.first_indexed_date, + p.last_indexed_date , + p.status, + p.status_change_date + FROM + projects p, + mines m + WHERE + p.mine_id = m.id + AND p.purl_name = $1 + AND m.purl_type = $2; + ` + var results []ComponentProjectStatus + err = m.q.SelectContext(m.ctx, &results, query, purlName, purl.Type) + if err != nil { + m.s.Errorf("Failed to query component status for %v: %v", purlName, err) + return nil, fmt.Errorf("failed to query component status: %v", err) + } + if len(results) == 0 { + m.s.Warnf("No status found for %v", purlName) + return nil, fmt.Errorf("component not found") + } + status = results[0] + m.s.Debugf("Found status for %v", purlName) + return &status, nil +} + +// GetProjectStatusByPurl gets only the project-level status (no version information). +func (m *ComponentStatusModel) GetProjectStatusByPurl(purlString string) (*ComponentProjectStatus, error) { + if len(purlString) == 0 { + m.s.Errorf("Please specify a valid Purl String to query") + return nil, errors.New("please specify a valid Purl String to query") + } + purl, err := purlhelper.PurlFromString(purlString) + if err != nil { + return nil, err + } + purlName, err := purlhelper.PurlNameFromString(purlString) + if err != nil { + return nil, err + } + var status ComponentProjectStatus + query := ` + SELECT DISTINCT + p.purl_name, + p.component, + p.first_indexed_date, + p.last_indexed_date, + p.status, + p.status_change_date + FROM projects p + JOIN mines m ON p.mine_id = m.id + WHERE p.purl_name = $1 + AND m.purl_type = $2 + LIMIT 1 + ` + var results []ComponentProjectStatus + err = m.q.SelectContext(m.ctx, &results, query, purlName, purl.Type) + if err != nil { + m.s.Errorf("Failed to query project status for %v: %v", purlName, err) + return nil, fmt.Errorf("failed to query project status: %v", err) + } + if len(results) == 0 { + m.s.Warnf("No project status found for %v", purlName) + return nil, fmt.Errorf("component not found") + } + status = results[0] + m.s.Debugf("Found project status for %v", purlName) + return &status, nil +} diff --git a/pkg/models/component_status_test.go b/pkg/models/component_status_test.go new file mode 100644 index 0000000..553f850 --- /dev/null +++ b/pkg/models/component_status_test.go @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package models + +import ( + "context" + "fmt" + "testing" + + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "github.com/scanoss/go-grpc-helper/pkg/grpc/database" + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" + _ "modernc.org/sqlite" + myconfig "scanoss.com/components/pkg/config" +) + +// TestGetComponentStatusByPurlAndVersion tests retrieving status for a specific component version. +// +//goland:noinspection DuplicatedCode +func TestGetComponentStatusByPurlAndVersion(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := ctxzap.ToContext(context.Background(), zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db := sqliteSetup(t) + defer CloseDB(db) + conn := sqliteConn(t, ctx, db) + defer CloseConn(conn) + err = LoadTestSQLData(db, ctx, conn) + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + componentStatusModel := NewComponentStatusModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) + + // Test cases that should pass + passTestTable := []struct { + purl string + version string + }{ + { + purl: "pkg:npm/react", + version: "18.0.0", + }, + { + purl: "pkg:gem/tablestyle", + version: "0.1.0", + }, + } + + for _, test := range passTestTable { + fmt.Printf("Testing purl: %v, version: %v\n", test.purl, test.version) + status, err := componentStatusModel.GetComponentStatusByPurlAndVersion(test.purl, test.version) + if err != nil { + // It's ok if we don't find the specific version in test data + fmt.Printf("Version not found (expected): %v\n", err) + } else { + fmt.Printf("Status: %+v\n", status) + } + } + + // Test cases that should fail + failTestTable := []struct { + purl string + version string + }{ + { + purl: "", // Empty purl + version: "1.0.0", + }, + { + purl: "invalid-purl", // Invalid purl format + version: "1.0.0", + }, + } + + for _, test := range failTestTable { + _, err := componentStatusModel.GetComponentStatusByPurlAndVersion(test.purl, test.version) + if err == nil { + t.Errorf("An error was expected for purl: %v, version: %v", test.purl, test.version) + } + } +} + +// TestGetComponentStatusByPurl tests retrieving status for a component (without version). +// +//goland:noinspection DuplicatedCode +func TestGetComponentStatusByPurl(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := ctxzap.ToContext(context.Background(), zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db := sqliteSetup(t) + defer CloseDB(db) + conn := sqliteConn(t, ctx, db) + defer CloseConn(conn) + err = LoadTestSQLData(db, ctx, conn) + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + componentStatusModel := NewComponentStatusModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) + + // Test cases that should pass + passTestTable := []struct { + purl string + }{ + {purl: "pkg:npm/react"}, + {purl: "pkg:gem/tablestyle"}, + } + + for _, test := range passTestTable { + fmt.Printf("Testing purl: %v\n", test.purl) + status, err := componentStatusModel.GetComponentStatusByPurl(test.purl) + if err != nil { + fmt.Printf("Component not found (may be expected): %v\n", err) + } else { + fmt.Printf("Status: %+v\n", status) + } + } + + // Test cases that should fail + failTestTable := []struct { + purl string + }{ + {purl: ""}, // Empty purl + {purl: "invalid-purl"}, // Invalid purl format + {purl: "pkg:npm/NOEXIST"}, // Non-existent component + } + + for _, test := range failTestTable { + _, err := componentStatusModel.GetComponentStatusByPurl(test.purl) + if err == nil { + t.Errorf("An error was expected for purl: %v", test.purl) + } + } +} + +// TestGetProjectStatusByPurl tests retrieving project-level status only (no version info). +// +//goland:noinspection DuplicatedCode +func TestGetProjectStatusByPurl(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := ctxzap.ToContext(context.Background(), zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db := sqliteSetup(t) + defer CloseDB(db) + conn := sqliteConn(t, ctx, db) + defer CloseConn(conn) + err = LoadTestSQLData(db, ctx, conn) + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + componentStatusModel := NewComponentStatusModel(ctx, s, database.NewDBSelectContext(s, db, conn, myConfig.Database.Trace)) + + // Test cases that should pass + passTestTable := []struct { + purl string + }{ + {purl: "pkg:npm/react"}, + {purl: "pkg:gem/tablestyle"}, + } + + for _, test := range passTestTable { + fmt.Printf("Testing project status for purl: %v\n", test.purl) + status, err := componentStatusModel.GetProjectStatusByPurl(test.purl) + if err != nil { + fmt.Printf("Component not found (may be expected): %v\n", err) + } else { + fmt.Printf("Project Status: %+v\n", status) + if status.Component == "" { + t.Errorf("Expected non-empty component name for purl: %v", test.purl) + } + } + } + + // Test cases that should fail + failTestTable := []struct { + purl string + }{ + {purl: ""}, // Empty purl + {purl: "invalid-purl"}, // Invalid purl format + {purl: "pkg:npm/NOEXIST"}, // Non-existent component + } + + for _, test := range failTestTable { + _, err := componentStatusModel.GetProjectStatusByPurl(test.purl) + if err == nil { + t.Errorf("An error was expected for purl: %v", test.purl) + } + } +} diff --git a/pkg/models/components.go b/pkg/models/components.go index 8b274ba..6830659 100644 --- a/pkg/models/components.go +++ b/pkg/models/components.go @@ -19,9 +19,10 @@ package models import ( "context" "errors" + "strings" + "github.com/scanoss/go-grpc-helper/pkg/grpc/database" "go.uber.org/zap" - "strings" ) var defaultPurlType = "github" @@ -40,7 +41,7 @@ type Component struct { Component string `db:"component"` PurlType string `db:"purl_type"` PurlName string `db:"purl_name"` - Url string `db:"-"` + URL string `db:"-"` } func NewComponentModel(ctx context.Context, s *zap.SugaredLogger, q *database.DBQueryContext, likeOperator string) *ComponentModel { @@ -50,9 +51,8 @@ func NewComponentModel(ctx context.Context, s *zap.SugaredLogger, q *database.DB return &ComponentModel{ctx: ctx, s: s, q: q, likeOperator: likeOperator} } -// preProcessQueryJob Replace the clause #ORDER in the queries (if exist) according to the purlType +// preProcessQueryJob Replace the clause #ORDER in the queries (if exist) according to the purlType. func preProcessQueryJob(qListIn []QueryJob, purlType string) ([]QueryJob, error) { - if len(qListIn) == 0 { return []QueryJob{}, errors.New("cannot pre process query jobs empty or with limit less than 0") } @@ -68,7 +68,7 @@ func preProcessQueryJob(qListIn []QueryJob, purlType string) ([]QueryJob, error) } for i := range qList { - //Adds or remove the ORDER BY clause in SQL query + // Adds or remove the ORDER BY clause in SQL query qList[i].Query = strings.Replace(qList[i].Query, "#ORDER", mapPurlTypeToOrderByClause[purlType], 1) qList[i].Query = strings.TrimRight(qList[i].Query, " ") } @@ -293,24 +293,20 @@ func (m *ComponentModel) GetComponentsByVendorType(vendorName, purlType string, Args: []any{"%" + vendorName, purlType, 1, offset}, }, } - queryJobs, err := preProcessQueryJob(queryJobs, purlType) if err != nil { return []Component{}, err } - allComponents, _ := RunQueries[Component](m.q, m.ctx, queryJobs) allComponents = RemoveDuplicated[Component](allComponents) if limit < len(allComponents) { allComponents = allComponents[:limit] } - return allComponents, nil } func (m *ComponentModel) GetComponentsByNameVendorType(compName, vendor, purlType string, limit, offset int) ([]Component, error) { - if len(compName) == 0 || len(vendor) == 0 { m.s.Error("Please specify a valid Component Name to query") return []Component{}, errors.New("please specify a valid component Name to query") diff --git a/pkg/models/components_test.go b/pkg/models/components_test.go index b2a27a1..7bddbec 100644 --- a/pkg/models/components_test.go +++ b/pkg/models/components_test.go @@ -19,15 +19,17 @@ package models import ( "context" "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/scanoss/go-grpc-helper/pkg/grpc/database" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" _ "modernc.org/sqlite" myconfig "scanoss.com/components/pkg/config" - "testing" ) +//goland:noinspection DuplicatedCode func TestComponentsModel(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -103,7 +105,6 @@ func TestComponentsModel(t *testing.T) { t.Errorf("components.GetComponentsByVendorType() error = %v", err) } fmt.Printf("Components: %v\n", components) - } _, err = component.GetComponents("", "", 0, 0) @@ -128,7 +129,6 @@ func TestComponentsModel(t *testing.T) { } func TestPreProcessQueryJobs(t *testing.T) { - testTable := []struct { qList []QueryJob purlType string diff --git a/pkg/models/doc.go b/pkg/models/doc.go new file mode 100644 index 0000000..9e8bc26 --- /dev/null +++ b/pkg/models/doc.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2022 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Package models contains the models for the application +package models diff --git a/pkg/models/tests/all_urls.sql b/pkg/models/tests/all_urls.sql index 51852e0..56e40ab 100644 --- a/pkg/models/tests/all_urls.sql +++ b/pkg/models/tests/all_urls.sql @@ -13,11 +13,19 @@ CREATE TABLE all_urls version_id integer, license_id integer, purl_name text, + indexed_date text, + version_status text, + version_status_change_date text, primary key (package_hash, url, url_hash) ); -insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('4d66775f503b1e76582e7e5b2ea54d92', 'taballa.hp-PD', 'tablestyle', '0.0.10', '2013-08-26', 'https://rubygems.org/downloads/tablestyle-0.0.10.gem', '5a088240b44efa142be4b3c40f8ae9c1', 1, 'MIT', 'tablestyle', 99999999, 5614); +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('4d66775f503b1e76582e7e5b2ea54d92', 'taballa.hp-PD', 'tablestyle', '0.0.10', '2013-08-26', 'https://rubygems.org/downloads/tablestyle-0.0.10.gem', '5a088240b44efa142be4b3c40f8ae9c1', 1, 'MIT', 'tablestyle', 99999999, 5614, '2013-08-26', 'active', '2013-08-26'); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('bfada11fd2b2b8fa23943b8b6fe5cb3f', 'taballa.hp-PD', 'tablestyle', '0.0.12', '2013-08-26', 'https://rubygems.org/downloads/tablestyle-0.0.12.gem', '686dc352775b58652c9d9ddb2117f402', 1, 'MIT', 'tablestyle', 258510, 5614); +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('react18001234567890abcdef1234567890', 'Jeff Barczewski', 'react', '18.0.0', '2022-03-29', 'https://registry.npmjs.org/react/-/react-18.0.0.tgz', 'react18001234567890abcdef12345678', 2, 'MIT', 'react', 10094162, 5614, '2022-03-29', 'active', '2022-03-29'); +-- Test versions with clean semver for component status tests +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('react199hash1234567890abcdef12345', 'Jeff Barczewski', 'react', '1.99.0', '2015-01-01', 'https://registry.npmjs.org/react/-/react-1.99.0.tgz', 'react199hash1234567890abcdef123', 2, 'MIT', 'react', 20000001, 5614, '2015-01-01', 'active', '2015-01-01'); +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('react299hash1234567890abcdef12345', 'Jeff Barczewski', 'react', '2.99.0', '2016-01-01', 'https://registry.npmjs.org/react/-/react-2.99.0.tgz', 'react299hash1234567890abcdef123', 2, 'MIT', 'react', 20000002, 5614, '2016-01-01', 'active', '2016-01-01'); +insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id, indexed_date, version_status, version_status_change_date) values ('tablestyle099hash1234567890abcde', 'taballa.hp-PD', 'tablestyle', '0.99.0', '2013-07-10', 'https://rubygems.org/downloads/tablestyle-0.99.0.gem', 'tablestyle099hash1234567890abc', 1, 'MIT', 'tablestyle', 20000003, 5614, '2013-07-10', 'active', '2013-07-10'); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('f586d603a9cb2460c4517cffad6ad5e4', 'taballa.hp-PD', 'tablestyle', '0.0.7', null, 'https://rubygems.org/downloads/tablestyle-0.0.7.gem', '2a3251711e7010ca15d232ec4ec4fb16', 1, 'MIT', 'tablestyle', 3515237, 5614); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('b1cd1444c2f76e7564f57b0e047994a4', 'taballa.hp-PD', 'tablestyle', '0.0.4', '2013-07-08', 'https://rubygems.org/downloads/tablestyle-0.0.4.gem', 'b494b3e367d26b6ab2785ad3aee8afb7', 1, 'MIT', 'tablestyle', 10472506, 5614); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('952edab5837f5f0e59638dd791187725', 'taballa.hp-PD', 'tablestyle', '0.0.11', '2013-08-26', 'https://rubygems.org/downloads/tablestyle-0.0.11.gem', '59fc4cf45a7d1425303a5bb897a463f4', 1, 'MIT', 'tablestyle', 11435747, 5614); @@ -9110,3 +9118,11 @@ insert into all_urls (package_hash, vendor, component, version, date, url, url_h insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('4bc72a10b9ce921e3811b141de2aea9c', 'The gRPC Authors', 'grpcio', '1.12.1', '2018-06-05', 'https://files.pythonhosted.org/packages/c6/b8/47468178ba19143e89b2da778eed660b84136c0a877224e79cc3c1c3fd32/grpcio-1.12.1-cp35-cp35m-manylinux1_x86_64.whl', 'ed02ac8a0d68b08444ccb1f2e0ac095c', 3, 'Apache License 2.0', 'grpcio', 6355554, 850); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('a85e0fcdfb68bb767a730196df8e0900', 'The gRPC Authors', 'grpcio', '1.12.1', '2018-06-05', 'https://files.pythonhosted.org/packages/13/71/87628a8edec5bffc86c5443d2cb9a569c3b65c7ff0ad05d5e6ee68042297/grpcio-1.12.1-cp36-cp36m-manylinux1_i686.whl', 'ee9feb79e16668a823384a24667485ac', 3, 'Apache License 2.0', 'grpcio', 6355554, 850); insert into all_urls (package_hash, vendor, component, version, date, url, url_hash, mine_id, license, purl_name, version_id, license_id) values ('55771098c0dc1dd47d63504ad795e595', 'The gRPC Authors', 'grpcio', '1.12.1', '2018-06-05', 'https://files.pythonhosted.org/packages/f7/db/fc084f59804a32a8d6efb467896a505f4dc93ea89ec44da856b91f05a5cb/grpcio-1.12.1-cp35-cp35m-manylinux1_i686.whl', 'f1c10eeaf3d8a7dae3d01ac9f46bc489', 3, 'MIT', 'grpcio', 6355554, 5614); + +-- Update rows that don't have indexed_date, version_status, and version_status_change_date +-- This fixes compatibility with go-models v0.7.0+ that expects these columns +UPDATE all_urls +SET indexed_date = COALESCE(indexed_date, date, '1970-01-01'), + version_status = COALESCE(version_status, 'active'), + version_status_change_date = COALESCE(version_status_change_date, date, '1970-01-01') +WHERE indexed_date IS NULL OR version_status IS NULL OR version_status_change_date IS NULL; diff --git a/pkg/models/tests/bad_sql.sql b/pkg/models/tests/bad_sql.sql index 8bf8a8f..819bd4e 100644 --- a/pkg/models/tests/bad_sql.sql +++ b/pkg/models/tests/bad_sql.sql @@ -1,3 +1,3 @@ +-- noinspection AnnotatorForFile -- noinspection SyntaxErrorForFile - CREATE TABLE failed; \ No newline at end of file diff --git a/pkg/models/tests/projects.sql b/pkg/models/tests/projects.sql index 967db2e..2b48947 100644 --- a/pkg/models/tests/projects.sql +++ b/pkg/models/tests/projects.sql @@ -615,9 +615,13 @@ CREATE TABLE projects verified text, license_id integer, git_license_id integer, + first_indexed_date text, + last_indexed_date text, + status text, + status_change_date text, PRIMARY KEY (mine_id, purl_name) ); -insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (1, 'taballa.hp-PD', 'tablestyle', '2013-07-05', '2013-08-26', 'MIT', 8, null, null, null, null, null, null, null, null, null, null, 'tablestyle', null, null, 5614, null); +insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id, first_indexed_date, last_indexed_date, status, status_change_date) values (1, 'taballa.hp-PD', 'tablestyle', '2013-07-05', '2013-08-26', 'MIT', 8, null, null, null, null, null, null, null, null, null, null, 'tablestyle', null, null, 5614, null, '2013-07-05', '2013-08-26', 'active', '2013-08-26'); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Sindre Sorhus', 'electron-debug', '2015-06-01', '2020-12-21', 'MIT', 28, 'sindresorhus', 'electron-debug', '2015-06-01', '2022-01-10', '2021-01-23', 693, 11, 55, 'MIT', 5, 'electron-debug', 'sindresorhus/electron-debug', '2022-01-11', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Shane Harris', 'node-blob', '2019-03-21', '2019-03-21', 'MIT', 2, 'shaneharris', 'node-blob', null, null, null, null, null, null, null, 5, 'node-blob', 'shaneharris/node-blob', '2021-05-24', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Gergely Hornich', 'sort-paths', '2016-09-03', '2018-08-18', 'MIT', 3, 'ghornich', 'sort-paths', null, null, null, null, null, null, null, 5, 'sort-paths', 'ghornich/sort-paths', '2021-05-24', 5614, null); @@ -634,7 +638,7 @@ insert into projects (mine_id, vendor, component, first_version_date, latest_ver insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Jake Zatecky', 'react-checkbox-tree', '2016-02-04', '2021-08-09', 'MIT', 47, 'jakezatecky', 'react-checkbox-tree', '2016-02-04', '2022-01-09', '2022-01-03', 552, 80, 165, 'MIT', 5, 'react-checkbox-tree', 'jakezatecky/react-checkbox-tree', '2022-01-11', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'React Training', 'history', '2012-04-29', '2021-12-17', 'MIT', 99, 'ReactTraining', 'history', '2015-07-18', '2021-08-12', '2021-08-12', 7099, 115, 841, 'MIT', 5, 'history', 'reacttraining/history', '2021-08-12', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Nikhil Marathe', 'uuid', '2011-03-31', '2021-11-29', 'MIT', 34, 'uuidjs', 'uuid', '2010-12-28', '2022-01-11', '2022-01-04', 11878, 20, 797, 'MIT', 5, 'uuid', 'uuidjs/uuid', '2022-01-11', 5614, null); -insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Jeff Barczewski', 'react', '2011-10-26', '2021-12-28', 'MIT', 739, 'facebook', 'react', '2013-05-24', '2022-01-12', '2022-01-11', 180572, 915, 36701, 'MIT', 5, 'react', 'facebook/react', '2022-01-11', 5614, null); +insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id, first_indexed_date, last_indexed_date, status, status_change_date) values (2, 'Jeff Barczewski', 'react', '2011-10-26', '2021-12-28', 'MIT', 739, 'facebook', 'react', '2013-05-24', '2022-01-12', '2022-01-11', 180572, 915, 36701, 'MIT', 5, 'react', 'facebook/react', '2022-01-11', 5614, null, '2011-10-26', '2022-01-11', 'active', '2022-01-11'); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'React Training', 'react-router-dom', '2016-12-14', '2021-12-17', 'MIT', 64, 'ReactTraining', 'react-router', '2014-05-16', '2021-08-12', '2021-08-11', 43727, 59, 8505, 'MIT', 5, 'react-router-dom', 'reacttraining/react-router', '2021-08-12', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Felix Geisendörfer', 'form-data', '2011-05-16', '2021-02-15', 'MIT', 38, 'form-data', 'form-data', '2011-05-16', '2022-01-11', '2021-11-30', 1962, 111, 256, 'MIT', 5, 'form-data', 'form-data/form-data', '2022-01-11', 5614, null); insert into projects (mine_id, vendor, component, first_version_date, latest_version_date, license, versions, source_vendor, source_component, git_created_at, git_updated_at, git_pushed_at, git_stars, git_issues, git_forks, git_license, source_mine_id, purl_name, source_purl_name, verified, license_id, git_license_id) values (2, 'Toru Nagashima', 'abort-controller', '2017-09-29', '2019-03-30', 'MIT', 11, 'mysticatea', 'abort-controller', '2017-09-29', '2021-12-30', '2021-03-30', 258, 17, 25, 'MIT', 5, 'abort-controller', 'mysticatea/abort-controller', '2022-01-11', 5614, null); diff --git a/pkg/models/tests/versions.sql b/pkg/models/tests/versions.sql index 3952288..4207dcc 100644 --- a/pkg/models/tests/versions.sql +++ b/pkg/models/tests/versions.sql @@ -237,6 +237,10 @@ insert into versions (id, version_name, semver) values (11766524, '0.5.14', 'v0. insert into versions (id, version_name, semver) values (6854887, '18.0.0-alpha-02f411578-20211019', 'v18.0.0-alpha-02f411578-20211019'); insert into versions (id, version_name, semver) values (12617765, '0.10.3', ''); insert into versions (id, version_name, semver) values (6924596, '1.38.0', ''); +-- Test versions with clean semver for component status tests +insert into versions (id, version_name, semver) values (20000001, '1.99.0', 'v1.99.0'); +insert into versions (id, version_name, semver) values (20000002, '2.99.0', 'v2.99.0'); +insert into versions (id, version_name, semver) values (20000003, '0.99.0', 'v0.99.0'); insert into versions (id, version_name, semver) values (11401945, '1.0.0rc1', 'v1.0.0-rc1'); insert into versions (id, version_name, semver) values (3922891, '3.4.0', ''); insert into versions (id, version_name, semver) values (1318167, '13.5.1', ''); diff --git a/pkg/protocol/rest/server.go b/pkg/protocol/rest/server.go index 5faccbd..d331d13 100644 --- a/pkg/protocol/rest/server.go +++ b/pkg/protocol/rest/server.go @@ -42,8 +42,8 @@ func RunServer(config *myconfig.ServerConfig, ctx context.Context, grpcPort, htt go func() { ctx2, cancel := context.WithCancel(ctx) defer cancel() - if err := pb.RegisterComponentsHandlerFromEndpoint(ctx2, mux, grpcGateway, opts); err != nil { - zlog.S.Panicf("Failed to start HTTP gateway %v", err) + if err2 := pb.RegisterComponentsHandlerFromEndpoint(ctx2, mux, grpcGateway, opts); err2 != nil { + zlog.S.Panicf("Failed to start HTTP gateway %v", err2) } gw.StartGateway(srv, config.TLS.CertFile, config.TLS.KeyFile, startTLS) }() diff --git a/pkg/service/component_service.go b/pkg/service/component_service.go index ab08a16..cd634df 100644 --- a/pkg/service/component_service.go +++ b/pkg/service/component_service.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2018-2022 SCANOSS.COM + * Copyright (C) 2018-2026 SCANOSS.COM * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -49,14 +49,14 @@ func NewComponentServer(db *sqlx.DB, config *myconfig.ServerConfig) pb.Component } } -// Echo sends back the same message received +// Echo sends back the same message received. func (d componentServer) Echo(ctx context.Context, request *common.EchoRequest) (*common.EchoResponse, error) { s := ctxzap.Extract(ctx).Sugar() s.Infof("Received (%v): %v", ctx, request.GetMessage()) return &common.EchoResponse{Message: request.GetMessage()}, nil } -// SearchComponents and retrieves a list of components +// SearchComponents and retrieves a list of components. func (d componentServer) SearchComponents(ctx context.Context, request *pb.CompSearchRequest) (*pb.CompSearchResponse, error) { requestStartTime := time.Now() // Capture the scan start time s := ctxzap.Extract(ctx).Sugar() @@ -76,7 +76,7 @@ func (d componentServer) SearchComponents(ctx context.Context, request *pb.CompS } // Search the KB for information about the components - compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace)) + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) dtoComponents, err := compUc.SearchComponents(dtoRequest) if err != nil { status := se.HandleServiceError(ctx, s, err) @@ -106,18 +106,17 @@ func (d componentServer) SearchComponents(ctx context.Context, request *pb.CompS } func (d componentServer) GetComponentVersions(ctx context.Context, request *pb.CompVersionRequest) (*pb.CompVersionResponse, error) { - requestStartTime := time.Now() // Capture the scan start time s := ctxzap.Extract(ctx).Sugar() s.Info("Processing component versions request...") - //Verify the input request + // Verify the input request if len(request.Purl) == 0 { status := se.HandleServiceError(ctx, s, se.NewBadRequestError("No purl supplied", nil)) status.Db = d.getDBVersion() status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} return &pb.CompVersionResponse{Status: status}, nil } - //Convert the request to internal DTO + // Convert the request to internal DTO dtoRequest, err := convertCompVersionsInput(s, request) if err != nil { status := se.HandleServiceError(ctx, s, err) @@ -126,7 +125,7 @@ func (d componentServer) GetComponentVersions(ctx context.Context, request *pb.C return &pb.CompVersionResponse{Status: status}, nil } // Creates the use case - compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace)) + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) dtoOutput, err := compUc.GetComponentVersions(dtoRequest) if err != nil { status := se.HandleServiceError(ctx, s, err) @@ -171,6 +170,75 @@ func telemetryCompVersionRequestTime(ctx context.Context, config *myconfig.Serve } } +// GetComponentStatus retrieves status information for a specific component. +func (d componentServer) GetComponentStatus(ctx context.Context, request *common.ComponentRequest) (*pb.ComponentStatusResponse, error) { + s := ctxzap.Extract(ctx).Sugar() + s.Info("Processing component status request...") + // Verify the input request + if len(request.Purl) == 0 { + s.Error("No purl supplied") + return &pb.ComponentStatusResponse{}, se.NewBadRequestError("No purl supplied", nil) + } + // Convert the request to internal DTO + dtoRequest, err := convertComponentStatusInput(s, request) + if err != nil { + s.Errorf("Failed to convert component status input: %v", err) + return &pb.ComponentStatusResponse{}, err + } + // Create the use case + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) + dtoOutput, err := compUc.GetComponentStatus(dtoRequest) + if err != nil { + s.Errorf("Failed to get component status: %v", err) + return &pb.ComponentStatusResponse{}, err + } + // Convert the output to protobuf + statusResponse := convertComponentStatusOutput(dtoOutput) + return statusResponse, nil +} + +// GetComponentsStatus retrieves status information for multiple components. +func (d componentServer) GetComponentsStatus(ctx context.Context, request *common.ComponentsRequest) (*pb.ComponentsStatusResponse, error) { + s := ctxzap.Extract(ctx).Sugar() + s.Info("Processing components status request...") + // Verify the input request + if len(request.Components) == 0 { + status := se.HandleServiceError(ctx, s, se.NewBadRequestError("No components supplied", nil)) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.ComponentsStatusResponse{Status: status}, nil + } + // Convert the request to internal DTO + dtoRequest, err := convertComponentsStatusInput(s, request) + if err != nil { + status := se.HandleServiceError(ctx, s, err) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.ComponentsStatusResponse{Status: status}, nil + } + // Create the use case + compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace), d.config.GetStatusMapper()) + dtoOutput, err := compUc.GetComponentsStatus(dtoRequest) + if err != nil { + status := se.HandleServiceError(ctx, s, err) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.ComponentsStatusResponse{Status: status}, nil + } + // Convert the output to protobuf + statusResponse := convertComponentsStatusOutput(dtoOutput) + // Set the status and respond with the data + return &pb.ComponentsStatusResponse{ + Components: statusResponse.Components, + Status: &common.StatusResponse{ + Status: common.StatusCode_SUCCESS, + Message: "Success", + Db: d.getDBVersion(), + Server: &common.StatusResponse_Server{Version: d.config.App.Version}, + }, + }, nil +} + // getDBVersion fetches the database version from the db_version table. // Returns nil if the table doesn't exist or query fails (backward compatibility). func (d componentServer) getDBVersion() *common.StatusResponse_DB { diff --git a/pkg/service/component_service_test.go b/pkg/service/component_service_test.go index a5452fb..368c19a 100644 --- a/pkg/service/component_service_test.go +++ b/pkg/service/component_service_test.go @@ -19,18 +19,22 @@ package service import ( "context" "encoding/json" + "reflect" + "testing" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/jmoiron/sqlx" common "github.com/scanoss/papi/api/commonv2" pb "github.com/scanoss/papi/api/componentsv2" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" _ "modernc.org/sqlite" - "reflect" myconfig "scanoss.com/components/pkg/config" "scanoss.com/components/pkg/models" - "testing" ) +const appVersion = "test-version" + +//goland:noinspection DuplicatedCode func TestComponentServer_Echo(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -48,7 +52,7 @@ func TestComponentServer_Echo(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } - myConfig.App.Version = "test-version" + myConfig.App.Version = appVersion s := NewComponentServer(db, myConfig) type args struct { @@ -86,6 +90,7 @@ func TestComponentServer_Echo(t *testing.T) { } } +//goland:noinspection DuplicatedCode func TestComponentServer_SearchComponents(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -107,7 +112,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } - myConfig.App.Version = "test-version" + myConfig.App.Version = appVersion s := NewComponentServer(db, myConfig) var compRequestData = `{ @@ -139,7 +144,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { ctx: ctx, req: &compReq, }, - want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No components found matching the search criteria", Server: &common.StatusResponse_Server{Version: "test-version"}}}, + want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No components found matching the search criteria", Server: &common.StatusResponse_Server{Version: appVersion}}}, }, { name: "Search for a empty request", @@ -148,7 +153,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { ctx: ctx, req: &pb.CompSearchRequest{}, }, - want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No data supplied", Server: &common.StatusResponse_Server{Version: "test-version"}}}, + want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No data supplied", Server: &common.StatusResponse_Server{Version: appVersion}}}, wantErr: false, }, } @@ -167,6 +172,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { } } +//goland:noinspection DuplicatedCode func TestComponentServer_GetComponentVersions(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -188,7 +194,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } - myConfig.App.Version = "test-version" + myConfig.App.Version = appVersion s := NewComponentServer(db, myConfig) var compVersionRequestData = `{ @@ -219,7 +225,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { ctx: ctx, req: &compVersionReq, }, - want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_SUCCESS, Message: "Success", Server: &common.StatusResponse_Server{Version: "test-version"}}}, + want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_SUCCESS, Message: "Success", Server: &common.StatusResponse_Server{Version: appVersion}}}, }, { name: "Search for a empty request", @@ -228,7 +234,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { ctx: ctx, req: &pb.CompVersionRequest{}, }, - want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No purl supplied", Server: &common.StatusResponse_Server{Version: "test-version"}}}, + want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No purl supplied", Server: &common.StatusResponse_Server{Version: appVersion}}}, wantErr: false, }, } @@ -246,3 +252,81 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { }) } } + +//goland:noinspection DuplicatedCode +func TestComponentServer_GetComponentStatus(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := context.Background() + ctx = ctxzap.ToContext(ctx, zlog.L) + db, err := sqlx.Connect("sqlite", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer models.CloseDB(db) + err = models.LoadTestSQLData(db, nil, nil) + if err != nil { + t.Fatalf("an error '%s' was not expected when loading test data", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.App.Version = appVersion + s := NewComponentServer(db, myConfig) + + type args struct { + ctx context.Context + req *common.ComponentRequest + } + tests := []struct { + name string + s pb.ComponentsServer + args args + wantErr bool + checkFn func(*testing.T, *pb.ComponentStatusResponse) + }{ + { + name: "Get status for react npm package", + s: s, + args: args{ + ctx: ctx, + req: &common.ComponentRequest{ + Purl: "pkg:npm/react", + Requirement: "^18.0.0", + }, + }, + wantErr: false, + checkFn: func(t *testing.T, resp *pb.ComponentStatusResponse) { + if resp == nil { + t.Error("Expected non-nil response") + } + }, + }, + { + name: "Get status with empty purl", + s: s, + args: args{ + ctx: ctx, + req: &common.ComponentRequest{}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.s.GetComponentStatus(tt.args.ctx, tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("service.GetComponentStatus() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.checkFn != nil && err == nil { + tt.checkFn(t, got) + } + }) + } +} diff --git a/pkg/service/component_support.go b/pkg/service/component_support.go index 3eb1485..0e26a66 100644 --- a/pkg/service/component_support.go +++ b/pkg/service/component_support.go @@ -3,6 +3,8 @@ package service import ( "encoding/json" "errors" + + "github.com/scanoss/go-grpc-helper/pkg/grpc/domain" pb "github.com/scanoss/papi/api/componentsv2" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/metric" @@ -26,6 +28,15 @@ func setupMetrics() { oltpMetrics.compVersionHistogram, _ = meter.Int64Histogram("comp.versions.req_time", metric.WithDescription("The time taken to run a comp versions request (ms)")) } +// convertSearchComponentInput converts a gRPC CompSearchRequest into a ComponentSearchInput DTO. +// It marshals the gRPC request to JSON and then unmarshals it into the internal DTO format. +// +// Parameters: +// - s: Sugared logger for error logging +// - request: gRPC component search request +// +// Returns: +// - ComponentSearchInput DTO or BadRequestError if conversion fails func convertSearchComponentInput(s *zap.SugaredLogger, request *pb.CompSearchRequest) (dtos.ComponentSearchInput, error) { data, err := json.Marshal(request) if err != nil { @@ -38,6 +49,15 @@ func convertSearchComponentInput(s *zap.SugaredLogger, request *pb.CompSearchReq return dtoRequest, nil } +// convertSearchComponentOutput converts a ComponentsSearchOutput DTO into a gRPC CompSearchResponse. +// It marshals the DTO to JSON and then unmarshals it into the gRPC response format. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentsSearchOutput DTO containing search results +// +// Returns: +// - gRPC CompSearchResponse or error if conversion fails func convertSearchComponentOutput(s *zap.SugaredLogger, output dtos.ComponentsSearchOutput) (*pb.CompSearchResponse, error) { data, err := json.Marshal(output) if err != nil { @@ -53,6 +73,15 @@ func convertSearchComponentOutput(s *zap.SugaredLogger, output dtos.ComponentsSe return &compResp, nil } +// convertCompVersionsInput converts a gRPC CompVersionRequest into a ComponentVersionsInput DTO. +// It marshals the gRPC request to JSON and then unmarshals it into the internal DTO format. +// +// Parameters: +// - s: Sugared logger for error logging +// - request: gRPC component version request +// +// Returns: +// - ComponentVersionsInput DTO or BadRequestError if conversion fails func convertCompVersionsInput(s *zap.SugaredLogger, request *pb.CompVersionRequest) (dtos.ComponentVersionsInput, error) { data, err := json.Marshal(request) if err != nil { @@ -65,6 +94,15 @@ func convertCompVersionsInput(s *zap.SugaredLogger, request *pb.CompVersionReque return dtoRequest, nil } +// convertCompVersionsOutput converts a ComponentVersionsOutput DTO into a gRPC CompVersionResponse. +// It marshals the DTO to JSON and then unmarshals it into the gRPC response format. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentVersionsOutput DTO containing component version data +// +// Returns: +// - gRPC CompVersionResponse or error if conversion fails func convertCompVersionsOutput(s *zap.SugaredLogger, output dtos.ComponentVersionsOutput) (*pb.CompVersionResponse, error) { data, err := json.Marshal(output) if err != nil { @@ -79,3 +117,122 @@ func convertCompVersionsOutput(s *zap.SugaredLogger, output dtos.ComponentVersio } return &compResp, nil } + +// convertComponentStatusInput converts a gRPC component status request into a ComponentStatusInput DTO. +// It accepts an interface{} to support both REST and gRPC request formats. +// It marshals the request to JSON and then unmarshals it into the internal DTO format. +// +// Parameters: +// - s: Sugared logger for error logging +// - request: Generic request interface (gRPC or REST format) +// +// Returns: +// - ComponentStatusInput DTO or BadRequestError if conversion fails +func convertComponentStatusInput(s *zap.SugaredLogger, request interface{}) (dtos.ComponentStatusInput, error) { + data, err := json.Marshal(request) + if err != nil { + return dtos.ComponentStatusInput{}, se.NewBadRequestError("Error parsing request data", err) + } + dtoRequest, err := dtos.ParseComponentStatusInput(s, data) + if err != nil { + return dtos.ComponentStatusInput{}, se.NewBadRequestError("Error parsing request data", err) + } + return dtoRequest, nil +} + +// convertComponentStatusOutput converts a ComponentStatusOutput DTO into a gRPC ComponentStatusResponse. +// It manually constructs the gRPC response structure, handling both success and error cases. +// The function handles optional fields like StatusChangeDate and VersionStatus appropriately. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentStatusOutput DTO containing component and version status data +// +// Returns: +// - gRPC ComponentStatusResponse with populated fields, or error if conversion fails +func convertComponentStatusOutput(output dtos.ComponentStatusOutput) *pb.ComponentStatusResponse { + response := &pb.ComponentStatusResponse{ + Purl: output.Purl, + Requirement: output.Requirement, + } + if output.ComponentStatus.ErrorCode == nil { + response.ComponentStatus = &pb.ComponentStatusResponse_ComponentStatus{ + FirstIndexedDate: output.ComponentStatus.FirstIndexedDate, + LastIndexedDate: output.ComponentStatus.LastIndexedDate, + Status: output.ComponentStatus.Status, + RepositoryStatus: output.ComponentStatus.RepositoryStatus, + } + if output.ComponentStatus.StatusChangeDate != "" { + response.ComponentStatus.StatusChangeDate = output.ComponentStatus.StatusChangeDate + } + response.Name = output.Name + } else { + response.ComponentStatus = &pb.ComponentStatusResponse_ComponentStatus{ + ErrorMessage: output.ComponentStatus.ErrorMessage, + ErrorCode: domain.StatusCodeToErrorCode(*output.ComponentStatus.ErrorCode), + } + return response + } + if output.VersionStatus != nil { + if output.VersionStatus.ErrorCode == nil { + response.VersionStatus = &pb.ComponentStatusResponse_VersionStatus{ + Version: output.VersionStatus.Version, + RepositoryStatus: output.VersionStatus.RepositoryStatus, + Status: output.VersionStatus.Status, + IndexedDate: output.VersionStatus.IndexedDate, + } + if output.VersionStatus.StatusChangeDate != "" { + response.VersionStatus.StatusChangeDate = output.VersionStatus.StatusChangeDate + } + } else { + response.VersionStatus = &pb.ComponentStatusResponse_VersionStatus{ + Version: output.VersionStatus.Version, + ErrorMessage: output.VersionStatus.ErrorMessage, + ErrorCode: domain.StatusCodeToErrorCode(*output.VersionStatus.ErrorCode), + } + } + } + + return response +} + +// convertComponentsStatusInput converts a gRPC components status request into a ComponentsStatusInput DTO. +// It accepts an interface{} to support both REST and gRPC request formats for batch status requests. +// It marshals the request to JSON and then unmarshals it into the internal DTO format. +// +// Parameters: +// - s: Sugared logger for error logging +// - request: Generic request interface (gRPC or REST format) containing multiple component status requests +// +// Returns: +// - ComponentsStatusInput DTO or BadRequestError if conversion fails +func convertComponentsStatusInput(s *zap.SugaredLogger, request interface{}) (dtos.ComponentsStatusInput, error) { + data, err := json.Marshal(request) + if err != nil { + return dtos.ComponentsStatusInput{}, se.NewBadRequestError("Error parsing request data", err) + } + dtoRequest, err := dtos.ParseComponentsStatusInput(s, data) + if err != nil { + return dtos.ComponentsStatusInput{}, se.NewBadRequestError("Error parsing request data", err) + } + return dtoRequest, nil +} + +// convertComponentsStatusOutput converts a ComponentsStatusOutput DTO into a gRPC ComponentsStatusResponse. +// It iterates through multiple component status results and converts each one using convertComponentStatusOutput. +// This function handles batch status responses for multiple components. +// +// Parameters: +// - s: Sugared logger for error logging +// - output: ComponentsStatusOutput DTO containing multiple component status results +// +// Returns: +// - gRPC ComponentsStatusResponse with all converted component status entries, or error if conversion fails +func convertComponentsStatusOutput(output dtos.ComponentsStatusOutput) *pb.ComponentsStatusResponse { + var statusResp pb.ComponentsStatusResponse + for _, c := range output.Components { + cs := convertComponentStatusOutput(c) + statusResp.Components = append(statusResp.Components, cs) + } + return &statusResp +} diff --git a/pkg/service/component_support_test.go b/pkg/service/component_support_test.go index 2efa0ba..28628b0 100644 --- a/pkg/service/component_support_test.go +++ b/pkg/service/component_support_test.go @@ -3,11 +3,12 @@ package service import ( "context" "fmt" + "testing" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" pb "github.com/scanoss/papi/api/componentsv2" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" "scanoss.com/components/pkg/dtos" - "testing" ) func TestConvertSearchComponentInput(t *testing.T) { @@ -28,7 +29,6 @@ func TestConvertSearchComponentInput(t *testing.T) { t.Errorf("Error generating dto from protobuff request: %v\n", err) } fmt.Printf("dto component input: %v\n", dto) - } func TestConvertSearchComponentOutput(t *testing.T) { @@ -44,7 +44,7 @@ func TestConvertSearchComponentOutput(t *testing.T) { { Component: "angular", Purl: "pkg:github/bclinkinbeard/angular", - Url: "https://github.com/bclinkinbeard/angular", + URL: "https://github.com/bclinkinbeard/angular", }, }} @@ -87,14 +87,14 @@ func TestConvertCompVersionsOutput(t *testing.T) { Component: dtos.ComponentOutput{ Component: "@angular/elements", Purl: "pkg:npm/%40angular/elements", - Url: "https://www.npmjs.com/package/%40angular/elements", + URL: "https://www.npmjs.com/package/%40angular/elements", Versions: []dtos.ComponentVersion{ { Version: "1.8.3", Licenses: []dtos.ComponentLicense{ { Name: "MIT", - SpdxId: "MIT", + SpdxID: "MIT", IsSpdx: true, }, }, @@ -108,5 +108,4 @@ func TestConvertCompVersionsOutput(t *testing.T) { t.Errorf("Error converting dto to protobuff request: %v\n", err) } fmt.Printf("dto component input: %v\n", protobuffOut) - } diff --git a/pkg/usecase/component.go b/pkg/usecase/component.go index 88cffe3..2d359af 100644 --- a/pkg/usecase/component.go +++ b/pkg/usecase/component.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2018-2022 SCANOSS.COM + * Copyright (C) 2018-2026 SCANOSS.COM * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,55 +14,65 @@ * along with this program. If not, see . */ +// Package usecase contains the business logic for the components API. package usecase import ( "context" "errors" "fmt" - se "scanoss.com/components/pkg/errors" "github.com/jmoiron/sqlx" + cmpHelper "github.com/scanoss/go-component-helper/componenthelper" "github.com/scanoss/go-grpc-helper/pkg/grpc/database" + "github.com/scanoss/go-grpc-helper/pkg/grpc/domain" purlhelper "github.com/scanoss/go-purl-helper/pkg" "go.uber.org/zap" + "scanoss.com/components/pkg/config" "scanoss.com/components/pkg/dtos" + se "scanoss.com/components/pkg/errors" "scanoss.com/components/pkg/models" ) type ComponentUseCase struct { - ctx context.Context - s *zap.SugaredLogger - q *database.DBQueryContext - components *models.ComponentModel - allUrl *models.AllUrlsModel + ctx context.Context + s *zap.SugaredLogger + q *database.DBQueryContext + components *models.ComponentModel + allURL *models.AllURLsModel + componentStatus *models.ComponentStatusModel + db *sqlx.DB + statusMapper *config.StatusMapper } -func NewComponents(ctx context.Context, s *zap.SugaredLogger, db *sqlx.DB, q *database.DBQueryContext) *ComponentUseCase { +func NewComponents(ctx context.Context, s *zap.SugaredLogger, db *sqlx.DB, q *database.DBQueryContext, statusMapper *config.StatusMapper) *ComponentUseCase { return &ComponentUseCase{ctx: ctx, s: s, q: q, - components: models.NewComponentModel(ctx, s, q, database.GetLikeOperator(db)), - allUrl: models.NewAllUrlModel(ctx, s, q), + components: models.NewComponentModel(ctx, s, q, database.GetLikeOperator(db)), + allURL: models.NewAllURLModel(ctx, s, q), + componentStatus: models.NewComponentStatusModel(ctx, s, q), + db: db, + statusMapper: statusMapper, } } func (c ComponentUseCase) SearchComponents(request dtos.ComponentSearchInput) (dtos.ComponentsSearchOutput, error) { var err error var searchResults []models.Component - - if len(request.Search) != 0 { + switch { + case len(request.Search) != 0: searchResults, err = c.components.GetComponents(request.Search, request.Package, request.Limit, request.Offset) - } else if len(request.Component) != 0 && len(request.Vendor) == 0 { + case len(request.Component) != 0 && len(request.Vendor) == 0: searchResults, err = c.components.GetComponentsByNameType(request.Component, request.Package, request.Limit, request.Offset) - } else if len(request.Component) == 0 && len(request.Vendor) != 0 { + case len(request.Component) == 0 && len(request.Vendor) != 0: searchResults, err = c.components.GetComponentsByVendorType(request.Vendor, request.Package, request.Limit, request.Offset) - } else if len(request.Component) != 0 && len(request.Vendor) != 0 { + case len(request.Component) != 0 && len(request.Vendor) != 0: searchResults, err = c.components.GetComponentsByNameVendorType(request.Component, request.Vendor, request.Package, request.Limit, request.Offset) } if err != nil { c.s.Errorf("Problem encountered searching for components: %v - %v.", request.Component, request.Package) } for i := range searchResults { - searchResults[i].Url, _ = purlhelper.ProjectUrl(searchResults[i].PurlName, searchResults[i].PurlType) + searchResults[i].URL, _ = purlhelper.ProjectUrl(searchResults[i].PurlName, searchResults[i].PurlType) } var componentsSearchResults []dtos.ComponentSearchOutput @@ -71,7 +81,7 @@ func (c ComponentUseCase) SearchComponents(request dtos.ComponentSearchInput) (d componentSearchResult.Name = component.Component componentSearchResult.Component = component.Component // Deprecated. Remove in future versions componentSearchResult.Purl = "pkg:" + component.PurlType + "/" + component.PurlName - componentSearchResult.Url = component.Url + componentSearchResult.URL = component.URL componentsSearchResults = append(componentsSearchResults, componentSearchResult) } if len(componentsSearchResults) == 0 { @@ -81,13 +91,11 @@ func (c ComponentUseCase) SearchComponents(request dtos.ComponentSearchInput) (d } func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInput) (dtos.ComponentVersionsOutput, error) { - if len(request.Purl) == 0 { c.s.Errorf("The request does not contains purl to retrieve component versions") return dtos.ComponentVersionsOutput{}, errors.New("the request does not contains purl to retrieve component versions") } - - allUrls, err := c.allUrl.GetUrlsByPurlString(request.Purl, request.Limit) + allUrls, err := c.allURL.GetUrlsByPurlString(request.Purl, request.Limit) if err != nil { c.s.Errorf("Problem encountered gettings URLs versions for: %v - %v.", request.Purl, err) return dtos.ComponentVersionsOutput{}, err @@ -96,22 +104,19 @@ func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInp if err != nil { c.s.Warnf("Problem encountered generating output component versions for: %v - %v.", request.Purl, err) } - purlName := purl.Name if purl.Type == "github" { purlName = fmt.Sprintf("%s/%s", purl.Namespace, purl.Name) } - projectURL, err := purlhelper.ProjectUrl(purlName, purl.Type) if err != nil { c.s.Warnf("Problem generating the project URL: %v - %v.", request.Purl, err) } - var output dtos.ComponentOutput output.Purl = request.Purl if len(allUrls) > 0 { output.Name = allUrls[0].Component - output.Url = projectURL + output.URL = projectURL output.Component = allUrls[0].Component output.Versions = []dtos.ComponentVersion{} for _, u := range allUrls { @@ -130,7 +135,7 @@ func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInp continue } license.Name = u.License - license.SpdxId = u.LicenseId + license.SpdxID = u.LicenseID license.IsSpdx = u.IsSpdx version.Licenses = append(version.Licenses, license) output.Versions = append(output.Versions, version) @@ -141,3 +146,165 @@ func (c ComponentUseCase) GetComponentVersions(request dtos.ComponentVersionsInp } return dtos.ComponentVersionsOutput{Component: output}, nil } + +func (c ComponentUseCase) GetComponentStatus(request dtos.ComponentStatusInput) (dtos.ComponentStatusOutput, error) { + if len(request.Purl) == 0 { + c.s.Errorf("The request does not contain purl to retrieve component status") + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("purl is required", errors.New("purl is required")) + } + results := cmpHelper.GetComponentsVersion(cmpHelper.ComponentVersionCfg{ + MaxWorkers: 1, + Ctx: c.ctx, + S: c.s, + DB: c.db, + Input: []cmpHelper.ComponentDTO{ + {Purl: request.Purl, Requirement: request.Requirement}, + }, + }) + if len(results) > 0 { + return c.handleComponentStatusResult(request, results[0]) + } + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("purl is required", errors.New("purl is required")) +} + +// handleComponentStatusResult routes the component status result to the appropriate handler based on status code. +func (c ComponentUseCase) handleComponentStatusResult(request dtos.ComponentStatusInput, result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { + //nolint:exhaustive + switch result.Status.StatusCode { + case domain.Success: + return c.handleSuccessStatus(request, result) + case domain.VersionNotFound: + return c.handleVersionNotFound(request, result) + case domain.InvalidPurl, domain.ComponentNotFound: + return c.handleErrorStatus(result) + default: + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("unknown status code", errors.New("unknown status code")) + } +} + +// handleSuccessStatus handles the case where both component and version are found. +func (c ComponentUseCase) handleSuccessStatus(request dtos.ComponentStatusInput, result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { + statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(result.Purl) + if errComp != nil { + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("error retrieving Component level data", errors.New("error retrieving Component Level Data")) + } + output := dtos.ComponentStatusOutput{ + Purl: request.Purl, + Name: statComponent.Component, + Requirement: request.Requirement, + ComponentStatus: c.buildComponentStatusInfo(statComponent), + } + // Try to get version-specific status + statusVersion := c.getVersionStatus(request.Purl, result) + if statusVersion != nil { + output.VersionStatus = c.buildVersionStatusOutput(statusVersion) + } + return output, nil +} + +// handleVersionNotFound handles the case where component exists but the version is not found. +func (c ComponentUseCase) handleVersionNotFound(request dtos.ComponentStatusInput, result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { + statComponent, errComp := c.componentStatus.GetComponentStatusByPurl(result.Purl) + if errComp != nil { + return dtos.ComponentStatusOutput{}, se.NewBadRequestError("error retrieving information", errors.New("error retrieving information")) + } + return dtos.ComponentStatusOutput{ + Purl: request.Purl, + Name: statComponent.Component, + Requirement: request.Requirement, + VersionStatus: &dtos.VersionStatusOutput{ + Version: request.Requirement, + ErrorMessage: &result.Status.Message, + ErrorCode: &result.Status.StatusCode, + }, + ComponentStatus: c.buildComponentStatusInfo(statComponent), + }, nil +} + +// handleErrorStatus handles error cases like InvalidPurl or ComponentNotFound. +func (c ComponentUseCase) handleErrorStatus(result cmpHelper.Component) (dtos.ComponentStatusOutput, error) { + return dtos.ComponentStatusOutput{ + Purl: result.Purl, + Name: "", + Requirement: result.Requirement, + ComponentStatus: &dtos.ComponentStatusInfo{ + ErrorMessage: &result.Status.Message, + ErrorCode: &result.Status.StatusCode, + }, + }, nil +} + +// buildComponentStatusInfo constructs a ComponentStatusInfo from a ComponentProjectStatus model. +func (c ComponentUseCase) buildComponentStatusInfo(statComponent *models.ComponentProjectStatus) *dtos.ComponentStatusInfo { + info := &dtos.ComponentStatusInfo{ + Status: c.statusMapper.MapStatus(statComponent.Status.String), + RepositoryStatus: statComponent.Status.String, + FirstIndexedDate: statComponent.FirstIndexedDate.String, + LastIndexedDate: statComponent.LastIndexedDate.String, + } + if statComponent.StatusChangeDate.String != "" { + info.StatusChangeDate = statComponent.StatusChangeDate.String + } + return info +} + +// getVersionStatus retrieves version-specific status information. +func (c ComponentUseCase) getVersionStatus(purl string, result cmpHelper.Component) *models.ComponentVersionStatus { + var statusVersion *models.ComponentVersionStatus + var errVersion error + if len(result.Version) > 0 { + statusVersion, errVersion = c.componentStatus.GetComponentStatusByPurlAndVersion(purl, result.Version) + } else if len(result.Requirement) > 0 { + statusVersion, errVersion = c.componentStatus.GetComponentStatusByPurlAndVersion(purl, result.Requirement) + } + if errVersion != nil { + c.s.Warnf("Problems getting version level status data for: %v - %v", purl, errVersion) + return nil + } + return statusVersion +} + +// buildVersionStatusOutput constructs a VersionStatusOutput from a ComponentVersionStatus model. +func (c ComponentUseCase) buildVersionStatusOutput(statusVersion *models.ComponentVersionStatus) *dtos.VersionStatusOutput { + output := &dtos.VersionStatusOutput{ + Version: statusVersion.Version, + Status: c.statusMapper.MapStatus(statusVersion.VersionStatus.String), + RepositoryStatus: statusVersion.VersionStatus.String, + IndexedDate: statusVersion.IndexedDate.String, + } + if statusVersion.VersionStatusChangeDate.String != "" { + output.StatusChangeDate = statusVersion.VersionStatusChangeDate.String + } + return output +} + +func (c ComponentUseCase) GetComponentsStatus(request dtos.ComponentsStatusInput) (dtos.ComponentsStatusOutput, error) { + if len(request.Components) == 0 { + c.s.Errorf("The request does not contain any components to retrieve status") + return dtos.ComponentsStatusOutput{}, se.NewBadRequestError("components array is required", errors.New("components array is required")) + } + var output dtos.ComponentsStatusOutput + output.Components = make([]dtos.ComponentStatusOutput, 0, len(request.Components)) + // Process each component request + for _, componentRequest := range request.Components { + componentStatus, err := c.GetComponentStatus(componentRequest) + if err != nil { + // For batch requests, we continue even if one component fails + // Add an error entry for this component + c.s.Warnf("Failed to get status for component: %v - %v", componentRequest.Purl, err) + errorMsg := err.Error() + errorStatus := dtos.ComponentStatusOutput{ + Purl: componentRequest.Purl, + Name: "", + Requirement: componentRequest.Requirement, + ComponentStatus: &dtos.ComponentStatusInfo{ + ErrorMessage: dtos.StringPtr(errorMsg), + }, + } + output.Components = append(output.Components, errorStatus) + } else { + output.Components = append(output.Components, componentStatus) + } + } + return output, nil +} diff --git a/pkg/usecase/component_test.go b/pkg/usecase/component_test.go index 5423fdc..ad89c80 100644 --- a/pkg/usecase/component_test.go +++ b/pkg/usecase/component_test.go @@ -19,6 +19,8 @@ package usecase import ( "context" "fmt" + "testing" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/jmoiron/sqlx" "github.com/scanoss/go-grpc-helper/pkg/grpc/database" @@ -27,9 +29,9 @@ import ( myconfig "scanoss.com/components/pkg/config" "scanoss.com/components/pkg/dtos" "scanoss.com/components/pkg/models" - "testing" ) +//goland:noinspection DuplicatedCode func TestComponentUseCase_SearchComponents(t *testing.T) { err := zlog.NewSugaredDevLogger() if err != nil { @@ -54,7 +56,7 @@ func TestComponentUseCase_SearchComponents(t *testing.T) { } myConfig.Database.Trace = true - compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace)) + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) goodTable := []dtos.ComponentSearchInput{ { @@ -84,11 +86,10 @@ func TestComponentUseCase_SearchComponents(t *testing.T) { fmt.Printf("Component-only search failed as expected: %v\n", err) // This is fine - some component searches may not find exact matches } - } +//goland:noinspection DuplicatedCode func TestComponentUseCase_GetComponentVersions(t *testing.T) { - err := zlog.NewSugaredDevLogger() if err != nil { t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) @@ -111,7 +112,7 @@ func TestComponentUseCase_GetComponentVersions(t *testing.T) { t.Fatalf("failed to load Config: %v", err) } - compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace)) + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) goodTable := []dtos.ComponentVersionsInput{ { @@ -148,6 +149,285 @@ func TestComponentUseCase_GetComponentVersions(t *testing.T) { if err == nil { t.Errorf("an error was expected when getting components version %v\n", err) } + } +} + +//goland:noinspection DuplicatedCode +func TestComponentUseCase_GetComponentStatus(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := context.Background() + ctx = ctxzap.ToContext(ctx, zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db, err := sqlx.Connect("sqlite", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer models.CloseDB(db) + err = models.LoadTestSQLData(db, nil, nil) + if err != nil { + t.Fatalf("an error '%s' was not expected when loading test data", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) + + // Good test cases + goodTable := []dtos.ComponentStatusInput{ + { + Purl: "pkg:npm/react", + Requirement: "^18.0.0", + }, + { + Purl: "pkg:gem/tablestyle", + Requirement: ">=0.1.0", + }, + } + + for i, input := range goodTable { + statusOut, err := compUc.GetComponentStatus(input) + if err != nil { + // It's ok if component is not found in test data + fmt.Printf("test case %d: Component status not found (may be expected): %v\n", i, err) + } else { + fmt.Printf("Status response: %+v\n", statusOut) + if statusOut.Purl != input.Purl { + t.Errorf("Expected purl %v, got %v", input.Purl, statusOut.Purl) + } + } + } + + // Fail test cases + failTestTable := []dtos.ComponentStatusInput{ + { + Purl: "", // Empty purl + Requirement: "1.0.0", + }, + } + + for i, input := range failTestTable { + _, err := compUc.GetComponentStatus(input) + if err == nil { + t.Errorf("test case %d: an error was expected for input %+v", i, input) + } + } +} + +//goland:noinspection DuplicatedCode +func TestComponentUseCase_GetComponentsStatus(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := context.Background() + ctx = ctxzap.ToContext(ctx, zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db, err := sqlx.Connect("sqlite", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer models.CloseDB(db) + err = models.LoadTestSQLData(db, nil, nil) + if err != nil { + t.Fatalf("an error '%s' was not expected when loading test data", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) + + // Test with multiple components + multipleInput := dtos.ComponentsStatusInput{ + Components: []dtos.ComponentStatusInput{ + { + Purl: "pkg:npm/react", + Requirement: "^18.0.0", + }, + { + Purl: "pkg:gem/tablestyle", + Requirement: ">=0.1.0", + }, + { + Purl: "", // This should fail + Requirement: "1.0.0", + }, + }, + } + + statusOut, err := compUc.GetComponentsStatus(multipleInput) + if err != nil { + t.Fatalf("Unexpected error getting components status: %v", err) + } + + if len(statusOut.Components) != 3 { + t.Errorf("Expected 3 component statuses, got %d", len(statusOut.Components)) + } + + fmt.Printf("Components Status response: %+v\n", statusOut) + + // Test with empty components array + emptyInput := dtos.ComponentsStatusInput{ + Components: []dtos.ComponentStatusInput{}, + } + + _, err = compUc.GetComponentsStatus(emptyInput) + if err == nil { + t.Errorf("Expected error for empty components array") + } +} + +// TestComponentUseCase_GetComponentStatus_AllCases tests all status code paths. +// +//goland:noinspection DuplicatedCode +func TestComponentUseCase_GetComponentStatus_AllCases(t *testing.T) { + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + ctx := context.Background() + ctx = ctxzap.ToContext(ctx, zlog.L) + s := ctxzap.Extract(ctx).Sugar() + db, err := sqlx.Connect("sqlite", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer models.CloseDB(db) + err = models.LoadTestSQLData(db, nil, nil) + if err != nil { + t.Fatalf("an error '%s' was not expected when loading test data", err) + } + myConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + myConfig.Database.Trace = true + + compUc := NewComponents(ctx, s, db, database.NewDBSelectContext(s, db, nil, myConfig.Database.Trace), myConfig.GetStatusMapper()) + + testCases := []struct { + name string + input dtos.ComponentStatusInput + expectError bool + expectedPurl string + checkStatusCode bool + statusShouldPass bool // true = Success, false = error status + }{ + { + name: "Success - Component and version found (react 1.99.0)", + input: dtos.ComponentStatusInput{ + Purl: "pkg:npm/react", + Requirement: "1.99.0", + }, + expectError: false, + expectedPurl: "pkg:npm/react", + checkStatusCode: true, + statusShouldPass: true, + }, + // TODO: Re-enable when go-component-helper fixes NULL handling bug with version constraints + // { + // name: "Success - Component and version found with range (react >=1.0.0)", + // input: dtos.ComponentStatusInput{ + // Purl: "pkg:npm/react", + // Requirement: ">=1.0.0", + // }, + // expectError: false, + // expectedPurl: "pkg:npm/react", + // checkStatusCode: true, + // statusShouldPass: true, + // }, + { + name: "Success - Component and version found (tablestyle 0.99.0)", + input: dtos.ComponentStatusInput{ + Purl: "pkg:gem/tablestyle", + Requirement: "0.99.0", + }, + expectError: false, + expectedPurl: "pkg:gem/tablestyle", + checkStatusCode: true, + statusShouldPass: true, + }, + { + name: "VersionNotFound - Component exists but version doesn't", + input: dtos.ComponentStatusInput{ + Purl: "pkg:npm/react", + Requirement: "999.0.0", + }, + expectError: false, + expectedPurl: "pkg:npm/react", + checkStatusCode: true, + statusShouldPass: false, + }, + { + name: "ComponentNotFound - Component doesn't exist", + input: dtos.ComponentStatusInput{ + Purl: "pkg:npm/nonexistent-package-xyz-123", + Requirement: "1.0.0", + }, + expectError: false, + expectedPurl: "pkg:npm/nonexistent-package-xyz-123", + checkStatusCode: true, + statusShouldPass: false, + }, + { + name: "InvalidPurl - Malformed purl", + input: dtos.ComponentStatusInput{ + Purl: "invalid-purl-format", + Requirement: "1.0.0", + }, + expectError: false, + checkStatusCode: true, + statusShouldPass: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + statusOut, err := compUc.GetComponentStatus(tc.input) + + if tc.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if tc.expectedPurl != "" && statusOut.Purl != tc.expectedPurl { + t.Errorf("Expected purl %v, got %v", tc.expectedPurl, statusOut.Purl) + } + if tc.statusShouldPass { + // For successful status, we should have component info + if statusOut.Name == "" { + t.Errorf("Expected non-empty component name for success case") + } + if statusOut.ComponentStatus == nil { + t.Errorf("Expected ComponentStatus for success case") + } + fmt.Printf("✓ %s: Success status received\n", tc.name) + } else { + // For error status, we should have error info + if statusOut.ComponentStatus != nil && statusOut.ComponentStatus.ErrorMessage == nil && statusOut.VersionStatus != nil && statusOut.VersionStatus.ErrorMessage == nil { + t.Errorf("Expected error message for failure case") + } + fmt.Printf("✓ %s: Error status received as expected\n", tc.name) + } + }) } }