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)
+ }
+ })
}
}