diff --git a/pkg/assembler/backends/arangodb/backend.go b/pkg/assembler/backends/arangodb/backend.go index e69e262756..d5dc320f9f 100644 --- a/pkg/assembler/backends/arangodb/backend.go +++ b/pkg/assembler/backends/arangodb/backend.go @@ -912,20 +912,6 @@ func getPreloadString(prefix, name string) string { return name } -// Topological queries: queries where node connectivity matters more than node type -func (c *arangoClient) Neighbors(ctx context.Context, node string, usingOnly []model.Edge) ([]model.Node, error) { - panic(fmt.Errorf("not implemented: Neighbors - Neighbors")) -} -func (c *arangoClient) Node(ctx context.Context, node string) (model.Node, error) { - panic(fmt.Errorf("not implemented: Node - Node")) -} -func (c *arangoClient) Nodes(ctx context.Context, nodes []string) ([]model.Node, error) { - panic(fmt.Errorf("not implemented: Nodes - Nodes")) -} -func (c *arangoClient) Path(ctx context.Context, subject string, target string, maxPathLength int, usingOnly []model.Edge) ([]model.Node, error) { - panic(fmt.Errorf("not implemented: Path - Path")) -} - func (c *arangoClient) Licenses(ctx context.Context, licenseSpec *model.LicenseSpec) ([]*model.License, error) { panic(fmt.Errorf("not implemented: Licenses")) } diff --git a/pkg/assembler/backends/arangodb/path.go b/pkg/assembler/backends/arangodb/path.go new file mode 100644 index 0000000000..a7b2dcadf0 --- /dev/null +++ b/pkg/assembler/backends/arangodb/path.go @@ -0,0 +1,50 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package arangodb + +import ( + "context" + "fmt" + "strings" + + "github.com/guacsec/guac/pkg/assembler/graphql/model" +) + +func (c *arangoClient) Path(ctx context.Context, startNodeID string, targetNodeID string, maxPathLength int, usingOnly []model.Edge) ([]model.Node, error) { + panic(fmt.Errorf("not implemented: Path")) +} + +func (c *arangoClient) Neighbors(ctx context.Context, nodeID string, usingOnly []model.Edge) ([]model.Node, error) { + panic(fmt.Errorf("not implemented: Neighbors")) +} + +func (c *arangoClient) Node(ctx context.Context, nodeID string) (model.Node, error) { + idSplit := strings.Split(nodeID, "/") + if len(idSplit) != 2 { + return nil, fmt.Errorf("invalid ID: %s", nodeID) + } + switch idSplit[0] { + case pkgVersionsStr, pkgNamesStr, pkgNamespacesStr, pkgTypesStr: + return c.buildPackageResponseFromID(ctx, nodeID, nil) + case srcNamesStr, srcNamespacesStr, srcTypesStr: + return c.buildSourceResponseFromID(ctx, nodeID, nil) + } + return nil, nil +} + +func (c *arangoClient) Nodes(ctx context.Context, nodeIDs []string) ([]model.Node, error) { + panic(fmt.Errorf("not implemented: Nodes")) +} diff --git a/pkg/assembler/backends/arangodb/path_test.go b/pkg/assembler/backends/arangodb/path_test.go new file mode 100644 index 0000000000..114c1abccf --- /dev/null +++ b/pkg/assembler/backends/arangodb/path_test.go @@ -0,0 +1,91 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +package arangodb + +import ( + "context" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/guacsec/guac/internal/testing/ptrfrom" + "github.com/guacsec/guac/internal/testing/testdata" + "github.com/guacsec/guac/pkg/assembler/graphql/model" +) + +func Test_Node(t *testing.T) { + ctx := context.Background() + arangArg := getArangoConfig() + err := deleteDatabase(ctx, arangArg) + if err != nil { + t.Fatalf("error deleting arango database: %v", err) + } + b, err := getBackend(ctx, arangArg) + if err != nil { + t.Fatalf("error creating arango backend: %v", err) + } + tests := []struct { + name string + pkgInput *model.PkgInputSpec + pkgFilter *model.PkgSpec + idInFilter bool + want *model.Package + wantErr bool + }{{ + name: "tensorflow empty version, ID search", + pkgInput: testdata.P1, + pkgFilter: &model.PkgSpec{ + Name: ptrfrom.String("tensorflow"), + }, + idInFilter: true, + want: testdata.P1out, + wantErr: false, + }, { + name: "openssl with match empty qualifiers", + pkgInput: testdata.P4, + pkgFilter: &model.PkgSpec{ + Name: ptrfrom.String("openssl"), + Namespace: ptrfrom.String("openssl.org"), + Version: ptrfrom.String("3.0.3"), + MatchOnlyEmptyQualifiers: ptrfrom.Bool(true), + }, + idInFilter: true, + want: testdata.P4out, + wantErr: false, + }} + ignoreID := cmp.FilterPath(func(p cmp.Path) bool { + return strings.Compare(".ID", p[len(p)-1].String()) == 0 + }, cmp.Ignore()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ingestedPkg, err := b.IngestPackage(ctx, *tt.pkgInput) + if (err != nil) != tt.wantErr { + t.Errorf("arangoClient.IngestPackage() error = %v, wantErr %v", err, tt.wantErr) + return + } + got, err := b.Node(ctx, ingestedPkg.Namespaces[0].Names[0].Versions[0].ID) + if (err != nil) != tt.wantErr { + t.Errorf("arangoClient.Packages() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want, got, ignoreID); diff != "" { + t.Errorf("Unexpected results. (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/assembler/backends/arangodb/pkg.go b/pkg/assembler/backends/arangodb/pkg.go index 6ca708fa2c..f31d0ba6ad 100644 --- a/pkg/assembler/backends/arangodb/pkg.go +++ b/pkg/assembler/backends/arangodb/pkg.go @@ -818,3 +818,323 @@ func removeInvalidCharFromProperty(key string) string { // be replaced by an "-" return strings.ReplaceAll(key, ".", "_") } + +// Builds a model.Package to send as GraphQL response, starting from id. +// The optional filter allows restricting output (on selection operations). +func (c *arangoClient) buildPackageResponseFromID(ctx context.Context, id string, filter *model.PkgSpec) (*model.Package, error) { + if filter != nil && filter.ID != nil { + if *filter.ID != id { + return nil, fmt.Errorf("ID does not match filter") + } + } + + idSplit := strings.Split(id, "/") + if len(idSplit) != 2 { + return nil, fmt.Errorf("invalid ID: %s", id) + } + + pvl := []*model.PackageVersion{} + if idSplit[0] == pkgVersionsStr { + var foundPkgVersion *model.PackageVersion + var err error + + foundPkgVersion, id, err = c.queryPkgVersionNodeByID(ctx, id, filter) + if err != nil { + return nil, fmt.Errorf("failed to get pkg version node by ID with error: %w", err) + } + pvl = append(pvl, foundPkgVersion) + } + + idSplit = strings.Split(id, "/") + if len(idSplit) != 2 { + return nil, fmt.Errorf("invalid ID: %s", id) + } + + pnl := []*model.PackageName{} + if idSplit[0] == pkgNamesStr { + var foundPkgName *model.PackageName + var err error + + foundPkgName, id, err = c.queryPkgNameNodeByID(ctx, id, filter, pvl) + if err != nil { + return nil, fmt.Errorf("failed to get pkg name node by ID with error: %w", err) + } + pnl = append(pnl, foundPkgName) + } + + idSplit = strings.Split(id, "/") + if len(idSplit) != 2 { + return nil, fmt.Errorf("invalid ID: %s", id) + } + + pnsl := []*model.PackageNamespace{} + if idSplit[0] == pkgNamespacesStr { + var foundPkgNamespace *model.PackageNamespace + var err error + + foundPkgNamespace, id, err = c.queryPkgNamespaceNodeByID(ctx, id, filter, pnl) + if err != nil { + return nil, fmt.Errorf("failed to get pkg namespace node by ID with error: %w", err) + } + pnsl = append(pnsl, foundPkgNamespace) + } + + idSplit = strings.Split(id, "/") + if len(idSplit) != 2 { + return nil, fmt.Errorf("invalid ID: %s", id) + } + + var p *model.Package + if idSplit[0] == pkgTypesStr { + var err error + + p, err = c.queryPkgTypeNodeByID(ctx, id, filter, pnsl) + if err != nil { + return nil, fmt.Errorf("failed to get pkg type node by ID with error: %w", err) + } + } + return p, nil +} + +func (c *arangoClient) queryPkgVersionNodeByID(ctx context.Context, id string, filter *model.PkgSpec) (*model.PackageVersion, string, error) { + values := map[string]any{} + arangoQueryBuilder := newForQuery(pkgVersionsStr, "pVersion") + arangoQueryBuilder.filter("pVersion", "_id", "==", "@id") + values["id"] = id + if filter != nil { + if filter.Version != nil { + arangoQueryBuilder.filter("pVersion", "version", "==", "@version") + values["version"] = *filter.Version + } + if filter.Subpath != nil { + arangoQueryBuilder.filter("pVersion", "subpath", "==", "@subpath") + values["subpath"] = *filter.Subpath + } + if filter.MatchOnlyEmptyQualifiers != nil { + if !*filter.MatchOnlyEmptyQualifiers { + if len(filter.Qualifiers) > 0 { + arangoQueryBuilder.filter("pVersion", "qualifier_list", "==", "@qualifier") + values["qualifier"] = getQualifiers(filter.Qualifiers) + } + } else { + arangoQueryBuilder.filterLength("pVersion", "qualifier_list", "==", 0) + } + } else { + if len(filter.Qualifiers) > 0 { + arangoQueryBuilder.filter("pVersion", "qualifier_list", "==", "@qualifier") + values["qualifier"] = getQualifiers(filter.Qualifiers) + } + } + } + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + 'version_id': pVersion._id, + 'version': pVersion.version, + 'subpath': pVersion.subpath, + 'qualifier_list': pVersion.qualifier_list, + 'parent': pVersion._parent + }`) + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "queryPkgVersionNodeByID") + if err != nil { + return nil, "", fmt.Errorf("failed to query for package version: %w, values: %v", err, values) + } + defer cursor.Close() + + type parsedPkgVersion struct { + VersionID string `json:"version_id"` + Version string `json:"version"` + Subpath string `json:"subpath"` + QualifierList []string `json:"qualifier_list"` + Parent string `json:"parent"` + } + + var collectedValues []parsedPkgVersion + for { + var doc parsedPkgVersion + _, err := cursor.ReadDocument(ctx, &doc) + if err != nil { + if driver.IsNoMoreDocuments(err) { + break + } else { + return nil, "", fmt.Errorf("failed to package version from cursor: %w", err) + } + } else { + collectedValues = append(collectedValues, doc) + } + } + + if len(collectedValues) != 1 { + return nil, "", fmt.Errorf("number of package version nodes found for ID: %s is greater than one", id) + } + + return &model.PackageVersion{ + ID: collectedValues[0].VersionID, + Version: collectedValues[0].Version, + Subpath: collectedValues[0].Subpath, + Qualifiers: getCollectedPackageQualifiers(collectedValues[0].QualifierList), + }, collectedValues[0].Parent, nil +} + +func (c *arangoClient) queryPkgNameNodeByID(ctx context.Context, id string, filter *model.PkgSpec, pvl []*model.PackageVersion) (*model.PackageName, string, error) { + values := map[string]any{} + arangoQueryBuilder := newForQuery(pkgNamesStr, "pName") + arangoQueryBuilder.filter("pName", "_id", "==", "@id") + values["id"] = id + + if filter != nil && filter.Name != nil { + arangoQueryBuilder.filter("pName", "name", "==", "@name") + values["name"] = *filter.Name + } + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + 'name_id': pName._id, + 'name': pName.name, + 'parent': pName._parent + }`) + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "queryPkgNameNodeByID") + if err != nil { + return nil, "", fmt.Errorf("failed to query for package name: %w, values: %v", err, values) + } + defer cursor.Close() + + type parsedPkgName struct { + NameID string `json:"name_id"` + Name string `json:"name"` + Parent string `json:"parent"` + } + + var collectedValues []parsedPkgName + for { + var doc parsedPkgName + _, err := cursor.ReadDocument(ctx, &doc) + if err != nil { + if driver.IsNoMoreDocuments(err) { + break + } else { + return nil, "", fmt.Errorf("failed to package name from cursor: %w", err) + } + } else { + collectedValues = append(collectedValues, doc) + } + } + + if len(collectedValues) != 1 { + return nil, "", fmt.Errorf("number of package name nodes found for ID: %s is greater than one", id) + } + + return &model.PackageName{ + ID: collectedValues[0].NameID, + Name: collectedValues[0].Name, + Versions: pvl, + }, collectedValues[0].Parent, nil +} + +func (c *arangoClient) queryPkgNamespaceNodeByID(ctx context.Context, id string, filter *model.PkgSpec, pnl []*model.PackageName) (*model.PackageNamespace, string, error) { + values := map[string]any{} + arangoQueryBuilder := newForQuery(pkgNamespacesStr, "pNs") + arangoQueryBuilder.filter("pNs", "_id", "==", "@id") + values["id"] = id + + if filter != nil && filter.Namespace != nil { + arangoQueryBuilder.filter("pNs", "namespace", "==", "@namespace") + values["namespace"] = *filter.Namespace + } + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + "namespace_id": pNs._id, + "namespace": pNs.namespace, + 'parent': pNs._parent + }`) + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "queryPkgNamespaceNodeByID") + if err != nil { + return nil, "", fmt.Errorf("failed to query for package namespace: %w, values: %v", err, values) + } + defer cursor.Close() + + type parsedPkgNamespace struct { + NamespaceID string `json:"namespace_id"` + Namespace string `json:"namespace"` + Parent string `json:"parent"` + } + + var collectedValues []parsedPkgNamespace + for { + var doc parsedPkgNamespace + _, err := cursor.ReadDocument(ctx, &doc) + if err != nil { + if driver.IsNoMoreDocuments(err) { + break + } else { + return nil, "", fmt.Errorf("failed to package namespace from cursor: %w", err) + } + } else { + collectedValues = append(collectedValues, doc) + } + } + + if len(collectedValues) != 1 { + return nil, "", fmt.Errorf("number of package namespace nodes found for ID: %s is greater than one", id) + } + + return &model.PackageNamespace{ + ID: collectedValues[0].NamespaceID, + Namespace: collectedValues[0].Namespace, + Names: pnl, + }, collectedValues[0].Parent, nil +} + +func (c *arangoClient) queryPkgTypeNodeByID(ctx context.Context, id string, filter *model.PkgSpec, pnsl []*model.PackageNamespace) (*model.Package, error) { + values := map[string]any{} + arangoQueryBuilder := newForQuery(pkgTypesStr, "pType") + arangoQueryBuilder.filter("pType", "_id", "==", "@id") + values["id"] = id + + if filter != nil && filter.Type != nil { + arangoQueryBuilder.filter("pType", "type", "==", "@pkgType") + values["pkgType"] = *filter.Type + } + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + "type_id": pType._id, + "type": pType.type, + }`) + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "queryPkgTypeNodeByID") + if err != nil { + return nil, fmt.Errorf("failed to query for package type: %w, values: %v", err, values) + } + defer cursor.Close() + + type parsedPkgType struct { + TypeID string `json:"type_id"` + PkgType string `json:"type"` + } + + var collectedValues []parsedPkgType + for { + var doc parsedPkgType + _, err := cursor.ReadDocument(ctx, &doc) + if err != nil { + if driver.IsNoMoreDocuments(err) { + break + } else { + return nil, fmt.Errorf("failed to package type from cursor: %w", err) + } + } else { + collectedValues = append(collectedValues, doc) + } + } + + if len(collectedValues) != 1 { + return nil, fmt.Errorf("number of package type nodes found for ID: %s is greater than one", id) + } + + return &model.Package{ + ID: collectedValues[0].TypeID, + Type: collectedValues[0].PkgType, + Namespaces: pnsl, + }, nil +} diff --git a/pkg/assembler/backends/arangodb/pkg_test.go b/pkg/assembler/backends/arangodb/pkg_test.go index 8ad79dde3e..e01b1818ff 100644 --- a/pkg/assembler/backends/arangodb/pkg_test.go +++ b/pkg/assembler/backends/arangodb/pkg_test.go @@ -814,3 +814,66 @@ func Test_IngestPackages(t *testing.T) { }) } } + +func Test_buildPackageResponseFromID(t *testing.T) { + ctx := context.Background() + arangArg := getArangoConfig() + err := deleteDatabase(ctx, arangArg) + if err != nil { + t.Fatalf("error deleting arango database: %v", err) + } + b, err := getBackend(ctx, arangArg) + if err != nil { + t.Fatalf("error creating arango backend: %v", err) + } + tests := []struct { + name string + pkgInput *model.PkgInputSpec + pkgFilter *model.PkgSpec + idInFilter bool + want *model.Package + wantErr bool + }{{ + name: "tensorflow empty version, ID search", + pkgInput: testdata.P3, + pkgFilter: &model.PkgSpec{ + Name: ptrfrom.String("tensorflow"), + Subpath: ptrfrom.String("saved_model_cli.py"), + }, + idInFilter: true, + want: testdata.P3out, + wantErr: false, + }, { + name: "openssl with match empty qualifiers", + pkgInput: testdata.P4, + pkgFilter: &model.PkgSpec{ + Name: ptrfrom.String("openssl"), + Namespace: ptrfrom.String("openssl.org"), + Version: ptrfrom.String("3.0.3"), + MatchOnlyEmptyQualifiers: ptrfrom.Bool(true), + }, + idInFilter: true, + want: testdata.P4out, + wantErr: false, + }} + ignoreID := cmp.FilterPath(func(p cmp.Path) bool { + return strings.Compare(".ID", p[len(p)-1].String()) == 0 + }, cmp.Ignore()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ingestedPkg, err := b.IngestPackage(ctx, *tt.pkgInput) + if (err != nil) != tt.wantErr { + t.Errorf("arangoClient.IngestPackage() error = %v, wantErr %v", err, tt.wantErr) + return + } + got, err := b.(*arangoClient).buildPackageResponseFromID(ctx, ingestedPkg.Namespaces[0].Names[0].Versions[0].ID, tt.pkgFilter) + if (err != nil) != tt.wantErr { + t.Errorf("arangoClient.buildPackageResponseFromID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want, got, ignoreID); diff != "" { + t.Errorf("Unexpected results. (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/assembler/backends/arangodb/src.go b/pkg/assembler/backends/arangodb/src.go index 41a7d4e10b..41ac10368a 100644 --- a/pkg/assembler/backends/arangodb/src.go +++ b/pkg/assembler/backends/arangodb/src.go @@ -530,3 +530,239 @@ func generateModelSource(srcTypeID, srcType, namespaceID, namespaceStr, nameID, } return &src } + +// Builds a model.Source to send as GraphQL response, starting from id. +// The optional filter allows restricting output (on selection operations). +func (c *arangoClient) buildSourceResponseFromID(ctx context.Context, id string, filter *model.SourceSpec) (*model.Source, error) { + if filter != nil && filter.ID != nil { + if *filter.ID != id { + return nil, fmt.Errorf("ID does not match filter") + } + } + + idSplit := strings.Split(id, "/") + if len(idSplit) != 2 { + return nil, fmt.Errorf("invalid ID: %s", id) + } + + snl := []*model.SourceName{} + if idSplit[0] == srcNamesStr { + var foundSrcName *model.SourceName + var err error + + foundSrcName, id, err = c.querySrcNameNodeByID(ctx, id, filter) + if err != nil { + return nil, fmt.Errorf("failed to get src name node by ID with error: %w", err) + } + snl = append(snl, foundSrcName) + } + + idSplit = strings.Split(id, "/") + if len(idSplit) != 2 { + return nil, fmt.Errorf("invalid ID: %s", id) + } + + snsl := []*model.SourceNamespace{} + if idSplit[0] == srcNamespacesStr { + var foundSrcNamespace *model.SourceNamespace + var err error + + foundSrcNamespace, id, err = c.querySrcNamespaceNodeByID(ctx, id, filter, snl) + if err != nil { + return nil, fmt.Errorf("failed to get src namespace node by ID with error: %w", err) + } + snsl = append(snsl, foundSrcNamespace) + } + + idSplit = strings.Split(id, "/") + if len(idSplit) != 2 { + return nil, fmt.Errorf("invalid ID: %s", id) + } + + var s *model.Source + if idSplit[0] == srcTypesStr { + var err error + s, err = c.querySrcTypeNodeByID(ctx, id, filter, snsl) + if err != nil { + return nil, fmt.Errorf("failed to get src type node by ID with error: %w", err) + } + } + return s, nil +} + +func (c *arangoClient) querySrcNameNodeByID(ctx context.Context, id string, filter *model.SourceSpec) (*model.SourceName, string, error) { + values := map[string]any{} + arangoQueryBuilder := newForQuery(srcNamesStr, "sName") + arangoQueryBuilder.filter("sName", "_id", "==", "@id") + values["id"] = id + if filter != nil { + if filter.Name != nil { + arangoQueryBuilder.filter("sName", "name", "==", "@name") + values["name"] = *filter.Name + } + if filter.Commit != nil { + arangoQueryBuilder.filter("sName", "commit", "==", "@commit") + values["commit"] = *filter.Commit + } + if filter.Tag != nil { + arangoQueryBuilder.filter("sName", "tag", "==", "@tag") + values["tag"] = *filter.Tag + } + } + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + "name_id": sName._id, + "name": sName.name, + "commit": sName.commit, + "tag": sName.tag, + 'parent': sName._parent + }`) + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "querySrcNameNodeByID") + if err != nil { + return nil, "", fmt.Errorf("failed to query for source name: %w, values: %v", err, values) + } + defer cursor.Close() + + type parsedSrcName struct { + NameID string `json:"name_id"` + Name string `json:"name"` + Commit string `json:"commit"` + Tag string `json:"tag"` + Parent string `json:"parent"` + } + + var collectedValues []parsedSrcName + for { + var doc parsedSrcName + _, err := cursor.ReadDocument(ctx, &doc) + if err != nil { + if driver.IsNoMoreDocuments(err) { + break + } else { + return nil, "", fmt.Errorf("failed to source name from cursor: %w", err) + } + } else { + collectedValues = append(collectedValues, doc) + } + } + + if len(collectedValues) != 1 { + return nil, "", fmt.Errorf("number of source name nodes found for ID: %s is greater than one", id) + } + + return &model.SourceName{ + ID: collectedValues[0].NameID, + Name: collectedValues[0].Name, + Tag: &collectedValues[0].Tag, + Commit: &collectedValues[0].Commit, + }, collectedValues[0].Parent, nil +} + +func (c *arangoClient) querySrcNamespaceNodeByID(ctx context.Context, id string, filter *model.SourceSpec, snl []*model.SourceName) (*model.SourceNamespace, string, error) { + values := map[string]any{} + arangoQueryBuilder := newForQuery(srcNamespacesStr, "sNs") + arangoQueryBuilder.filter("sNs", "_id", "==", "@id") + values["id"] = id + + if filter != nil && filter.Namespace != nil { + arangoQueryBuilder.filter("sNs", "namespace", "==", "@namespace") + values["namespace"] = *filter.Namespace + } + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + "namespace_id": sNs._id, + "namespace": sNs.namespace, + 'parent': sNs._parent + }`) + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "querySrcNamespaceNodeByID") + if err != nil { + return nil, "", fmt.Errorf("failed to query for source namespace: %w, values: %v", err, values) + } + defer cursor.Close() + + type parsedSrcNamespace struct { + NamespaceID string `json:"namespace_id"` + Namespace string `json:"namespace"` + Parent string `json:"parent"` + } + + var collectedValues []parsedSrcNamespace + for { + var doc parsedSrcNamespace + _, err := cursor.ReadDocument(ctx, &doc) + if err != nil { + if driver.IsNoMoreDocuments(err) { + break + } else { + return nil, "", fmt.Errorf("failed to source namespace from cursor: %w", err) + } + } else { + collectedValues = append(collectedValues, doc) + } + } + + if len(collectedValues) != 1 { + return nil, "", fmt.Errorf("number of source namespace nodes found for ID: %s is greater than one", id) + } + + return &model.SourceNamespace{ + ID: collectedValues[0].NamespaceID, + Namespace: collectedValues[0].Namespace, + Names: snl, + }, collectedValues[0].Parent, nil +} + +func (c *arangoClient) querySrcTypeNodeByID(ctx context.Context, id string, filter *model.SourceSpec, snsl []*model.SourceNamespace) (*model.Source, error) { + values := map[string]any{} + arangoQueryBuilder := newForQuery(srcTypesStr, "sType") + arangoQueryBuilder.filter("sType", "_id", "==", "@id") + values["id"] = id + + if filter != nil && filter.Type != nil { + arangoQueryBuilder.filter("sType", "type", "==", "@srcType") + values["srcType"] = *filter.Type + } + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + "type_id": sType._id, + "type":sType.type, + }`) + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "querySrcTypeNodeByID") + if err != nil { + return nil, fmt.Errorf("failed to query for source type: %w, values: %v", err, values) + } + defer cursor.Close() + + type parsedSrcType struct { + TypeID string `json:"type_id"` + SrcType string `json:"type"` + } + + var collectedValues []parsedSrcType + for { + var doc parsedSrcType + _, err := cursor.ReadDocument(ctx, &doc) + if err != nil { + if driver.IsNoMoreDocuments(err) { + break + } else { + return nil, fmt.Errorf("failed to source type from cursor: %w", err) + } + } else { + collectedValues = append(collectedValues, doc) + } + } + + if len(collectedValues) != 1 { + return nil, fmt.Errorf("number of source type nodes found for ID: %s is greater than one", id) + } + + return &model.Source{ + ID: collectedValues[0].TypeID, + Type: collectedValues[0].SrcType, + Namespaces: snsl, + }, nil +} diff --git a/pkg/assembler/backends/arangodb/src_test.go b/pkg/assembler/backends/arangodb/src_test.go index 014460cef5..55f65feb9f 100644 --- a/pkg/assembler/backends/arangodb/src_test.go +++ b/pkg/assembler/backends/arangodb/src_test.go @@ -365,3 +365,86 @@ func Test_SourceNamespaces(t *testing.T) { }) } } + +func Test_buildSourceResponseFromID(t *testing.T) { + ctx := context.Background() + arangArg := getArangoConfig() + err := deleteDatabase(ctx, arangArg) + if err != nil { + t.Fatalf("error deleting arango database: %v", err) + } + b, err := getBackend(ctx, arangArg) + if err != nil { + t.Fatalf("error creating arango backend: %v", err) + } + tests := []struct { + name string + srcInput *model.SourceInputSpec + srcFilter *model.SourceSpec + idInFilter bool + want *model.Source + wantErr bool + }{{ + name: "myrepo with tag", + srcInput: testdata.S1, + srcFilter: &model.SourceSpec{ + Name: ptrfrom.String("myrepo"), + }, + idInFilter: false, + want: testdata.S1out, + wantErr: false, + }, { + name: "myrepo with tag, ID search", + srcInput: testdata.S1, + srcFilter: &model.SourceSpec{ + Name: ptrfrom.String("myrepo"), + }, + idInFilter: true, + want: testdata.S1out, + wantErr: false, + }, { + name: "bobsrepo with commit", + srcInput: testdata.S4, + srcFilter: &model.SourceSpec{ + Namespace: ptrfrom.String("github.com/bob"), + Commit: ptrfrom.String("5e7c41f"), + }, + idInFilter: false, + want: testdata.S4out, + wantErr: false, + }, { + name: "bobsrepo with commit, type search", + srcInput: testdata.S4, + srcFilter: &model.SourceSpec{ + Type: ptrfrom.String("svn"), + Namespace: ptrfrom.String("github.com/bob"), + Commit: ptrfrom.String("5e7c41f"), + }, + idInFilter: false, + want: testdata.S4out, + wantErr: false, + }} + ignoreID := cmp.FilterPath(func(p cmp.Path) bool { + return strings.Compare(".ID", p[len(p)-1].String()) == 0 + }, cmp.Ignore()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ingestedPkg, err := b.IngestSource(ctx, *tt.srcInput) + if (err != nil) != tt.wantErr { + t.Errorf("arangoClient.IngestSource() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.idInFilter { + tt.srcFilter.ID = &ingestedPkg.Namespaces[0].Names[0].ID + } + got, err := b.(*arangoClient).buildSourceResponseFromID(ctx, ingestedPkg.Namespaces[0].Names[0].ID, tt.srcFilter) + if (err != nil) != tt.wantErr { + t.Errorf("arangoClient.buildSourceResponseFromID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want, got, ignoreID); diff != "" { + t.Errorf("Unexpected results. (-want +got):\n%s", diff) + } + }) + } +}