github.com/cobalt77/jfrog-client-go@v0.14.5/artifactory/services/utils/aqlquerybuilder.go (about)

     1  package utils
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  
     8  	"github.com/cobalt77/jfrog-client-go/utils/io/fileutils"
     9  
    10  	"github.com/cobalt77/jfrog-client-go/utils"
    11  )
    12  
    13  // Returns an AQL body string to search file in Artifactory by pattern, according the the specified arguments requirements.
    14  func CreateAqlBodyForSpecWithPattern(params *ArtifactoryCommonParams) (string, error) {
    15  	searchPattern := prepareSourceSearchPattern(params.Pattern, params.Target, true)
    16  	repoPathFileTriples := createRepoPathFileTriples(searchPattern, params.Recursive)
    17  	includeRoot := strings.Count(searchPattern, "/") < 2
    18  	triplesSize := len(repoPathFileTriples)
    19  
    20  	propsQueryPart, err := buildPropsQueryPart(params.Props, params.ExcludeProps)
    21  	if err != nil {
    22  		return "", err
    23  	}
    24  	itemTypeQuery := buildItemTypeQueryPart(params)
    25  	nePath := buildNePathPart(triplesSize == 0 || includeRoot)
    26  	excludeQuery := buildExcludeQueryPart(params, triplesSize == 0 || params.Recursive, params.Recursive)
    27  	releaseBundle, err := buildReleaseBundleQuery(params)
    28  	if err != nil {
    29  		return "", err
    30  	}
    31  
    32  	json := fmt.Sprintf(`{%s"$or":[`, propsQueryPart+itemTypeQuery+nePath+excludeQuery+releaseBundle)
    33  
    34  	// Get archive search parameters
    35  	archivePathFilePairs := createArchiveSearchParams(params)
    36  
    37  	json += handleRepoPathFileTriples(repoPathFileTriples, archivePathFilePairs, triplesSize) + "]}"
    38  	return json, nil
    39  }
    40  
    41  func createArchiveSearchParams(params *ArtifactoryCommonParams) []RepoPathFile {
    42  	var archivePathFilePairs []RepoPathFile
    43  
    44  	if params.ArchiveEntries != "" {
    45  		archiveSearchPattern := prepareSearchPattern(params.ArchiveEntries, false)
    46  		archivePathFilePairs = createPathFilePairs("", archiveSearchPattern, true)
    47  	}
    48  
    49  	return archivePathFilePairs
    50  }
    51  
    52  // Handle building aql query when having PathFilePairs
    53  func handleRepoPathFileTriples(pathFilePairs []RepoPathFile, archivePathFilePairs []RepoPathFile, pathPairSize int) string {
    54  	var query string
    55  	archivePathPairSize := len(archivePathFilePairs)
    56  
    57  	for i := 0; i < pathPairSize; i++ {
    58  		if archivePathPairSize > 0 {
    59  			query += handleArchiveSearch(pathFilePairs[i], archivePathFilePairs)
    60  		} else {
    61  			query += buildInnerQueryPart(pathFilePairs[i])
    62  		}
    63  
    64  		if i+1 < pathPairSize {
    65  			query += ","
    66  		}
    67  	}
    68  
    69  	return query
    70  }
    71  
    72  // Handle building aql query including archive search
    73  func handleArchiveSearch(triple RepoPathFile, archivePathFilePairs []RepoPathFile) string {
    74  	var query string
    75  	archivePathPairSize := len(archivePathFilePairs)
    76  	for i := 0; i < archivePathPairSize; i++ {
    77  		query += buildInnerArchiveQueryPart(triple, archivePathFilePairs[i].path, archivePathFilePairs[i].file)
    78  
    79  		if i+1 < archivePathPairSize {
    80  			query += ","
    81  		}
    82  	}
    83  	return query
    84  }
    85  
    86  func createAqlBodyForBuild(buildName, buildNumber string) string {
    87  	itemsPart :=
    88  		`{` +
    89  			`"artifact.module.build.name":"%s",` +
    90  			`"artifact.module.build.number":"%s"` +
    91  			`}`
    92  	return fmt.Sprintf(itemsPart, buildName, buildNumber)
    93  }
    94  
    95  func createAqlQueryForBuild(buildName, buildNumber, includeQueryPart string) string {
    96  	queryBody := createAqlBodyForBuild(buildName, buildNumber)
    97  	itemsPart := `items.find(%s)%s`
    98  	return fmt.Sprintf(itemsPart, queryBody, includeQueryPart)
    99  }
   100  
   101  //noinspection GoUnusedExportedFunction
   102  func CreateAqlQueryForNpm(npmName, npmVersion string) string {
   103  	itemsPart :=
   104  		`items.find({` +
   105  			`"@npm.name":"%s",` +
   106  			`"@npm.version":"%s"` +
   107  			`})%s`
   108  	return fmt.Sprintf(itemsPart, npmName, npmVersion, buildIncludeQueryPart([]string{"name", "repo", "path", "actual_sha1", "actual_md5"}))
   109  }
   110  
   111  func CreateAqlQueryForPypi(repo, file string) string {
   112  	itemsPart :=
   113  		`items.find({` +
   114  			`"repo": "%s",` +
   115  			`"$or": [{` +
   116  			`"$and":[{` +
   117  			`"path": {"$match": "*"},` +
   118  			`"name": {"$match": "%s"}` +
   119  			`}]` +
   120  			`}]` +
   121  			`})%s`
   122  	return fmt.Sprintf(itemsPart, repo, file, buildIncludeQueryPart([]string{"name", "repo", "path", "actual_md5", "actual_sha1"}))
   123  }
   124  
   125  func prepareSearchPattern(pattern string, repositoryExists bool) string {
   126  	addWildcardIfNeeded(&pattern, repositoryExists)
   127  	// Remove parenthesis
   128  	pattern = strings.Replace(pattern, "(", "", -1)
   129  	pattern = strings.Replace(pattern, ")", "", -1)
   130  	return pattern
   131  }
   132  
   133  func buildPropsQueryPart(props, excludeProps string) (string, error) {
   134  	propsQuery := ""
   135  	properties, err := ParseProperties(props, JoinCommas)
   136  	if err != nil {
   137  		return "", err
   138  	}
   139  	for _, v := range properties.Properties {
   140  		propsQuery += buildKeyValQueryPart(v.Key, v.Value) + `,`
   141  	}
   142  
   143  	excludePropsQuery := ""
   144  	excludeProperties, err := ParseProperties(excludeProps, JoinCommas)
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  	excludePropsLen := len(excludeProperties.Properties)
   149  	if excludePropsLen == 1 {
   150  		singleProp := &excludeProperties.Properties[0]
   151  		excludePropsQuery = buildExcludedKeyValQueryPart(singleProp.Key, singleProp.Value) + `,`
   152  	} else if excludePropsLen > 1 {
   153  		excludePropsQuery = `"$or":[`
   154  		for _, v := range excludeProperties.Properties {
   155  			excludePropsQuery += `{` + buildExcludedKeyValQueryPart(v.Key, v.Value) + `},`
   156  		}
   157  		excludePropsQuery = strings.TrimSuffix(excludePropsQuery, ",") + `],`
   158  	}
   159  	return propsQuery + excludePropsQuery, nil
   160  }
   161  
   162  func buildKeyValQueryPart(key string, value string) string {
   163  	return fmt.Sprintf(`"@%s":%s`, key, getAqlValue(value))
   164  }
   165  
   166  func buildExcludedKeyValQueryPart(key string, value string) string {
   167  	return fmt.Sprintf(`"@%s":{"$ne":%s}`, key, getAqlValue(value))
   168  }
   169  
   170  func buildItemTypeQueryPart(params *ArtifactoryCommonParams) string {
   171  	if params.IncludeDirs {
   172  		return `"type":"any",`
   173  	}
   174  	return ""
   175  }
   176  
   177  func buildNePathPart(includeRoot bool) string {
   178  	if !includeRoot {
   179  		return `"path":{"$ne":"."},`
   180  	}
   181  	return ""
   182  }
   183  
   184  func buildInnerQueryPart(triple RepoPathFile) string {
   185  	innerQueryPattern := `{"$and":` +
   186  		`[{` +
   187  		`"repo":%s,` +
   188  		`"path":%s,` +
   189  		`"name":%s` +
   190  		`}]}`
   191  	return fmt.Sprintf(innerQueryPattern, getAqlValue(triple.repo), getAqlValue(triple.path), getAqlValue(triple.file))
   192  }
   193  
   194  func buildInnerArchiveQueryPart(triple RepoPathFile, archivePath, archiveName string) string {
   195  	innerQueryPattern := `{"$and":` +
   196  		`[{` +
   197  		`"repo":%s,` +
   198  		`"path":%s,` +
   199  		`"name":%s,` +
   200  		`"archive.entry.path":%s,` +
   201  		`"archive.entry.name":%s` +
   202  		`}]}`
   203  	return fmt.Sprintf(innerQueryPattern, getAqlValue(triple.repo), getAqlValue(triple.path), getAqlValue(triple.file), getAqlValue(archivePath), getAqlValue(archiveName))
   204  }
   205  
   206  func buildExcludeQueryPart(params *ArtifactoryCommonParams, useLocalPath, recursive bool) string {
   207  	excludeQuery := ""
   208  	var excludeTriples []RepoPathFile
   209  	if len(params.GetExclusions()) > 0 {
   210  		for _, excludePattern := range params.GetExclusions() {
   211  			excludeTriples = append(excludeTriples, createRepoPathFileTriples(prepareSearchPattern(excludePattern, true), recursive)...)
   212  		}
   213  	} else {
   214  		// Support legacy exclude patterns. 'Exclude patterns' are deprecated and replaced by 'exclusions'.
   215  		for _, excludePattern := range params.GetExcludePatterns() {
   216  			excludeTriples = append(excludeTriples, createPathFilePairs("", prepareSearchPattern(excludePattern, false), recursive)...)
   217  		}
   218  	}
   219  
   220  	for _, excludeTriple := range excludeTriples {
   221  		excludePath := excludeTriple.path
   222  		if !useLocalPath && excludePath == "." {
   223  			excludePath = "*"
   224  		}
   225  		excludeRepoStr := ""
   226  		if excludeTriple.repo != "" {
   227  			excludeRepoStr = fmt.Sprintf(`"repo":{"$nmatch":"%s"},`, excludeTriple.repo)
   228  		}
   229  		excludeQuery += fmt.Sprintf(`"$or":[{%s"path":{"$nmatch":"%s"},"name":{"$nmatch":"%s"}}],`, excludeRepoStr, excludePath, excludeTriple.file)
   230  	}
   231  	return excludeQuery
   232  }
   233  
   234  func buildReleaseBundleQuery(params *ArtifactoryCommonParams) (string, error) {
   235  	bundleName, bundleVersion, err := parseNameAndVersion(params.Bundle, false)
   236  	if bundleName == "" || err != nil {
   237  		return "", err
   238  	}
   239  	itemsPart := `"$and":` +
   240  		`[{` +
   241  		`"release_artifact.release.name":%s,` +
   242  		`"release_artifact.release.version":%s` +
   243  		`}],`
   244  	return fmt.Sprintf(itemsPart, getAqlValue(bundleName), getAqlValue(bundleVersion)), nil
   245  }
   246  
   247  // Creates a list of basic required return fields. The list will include the sortBy field if needed.
   248  // If requiredArtifactProps is NONE or 'includePropertiesInAqlForSpec' return false,
   249  // "property" field won't be included due to a limitation in the AQL implementation in Artifactory.
   250  func getQueryReturnFields(specFile *ArtifactoryCommonParams, requiredArtifactProps RequiredArtifactProps) []string {
   251  	returnFields := []string{"name", "repo", "path", "actual_md5", "actual_sha1", "size", "type", "modified", "created"}
   252  	if !includePropertiesInAqlForSpec(specFile) {
   253  		// Sort dose not work when property is in the include section. in this case we will append properties in later stage.
   254  		return appendMissingFields(specFile.SortBy, returnFields)
   255  	}
   256  	if requiredArtifactProps != NONE {
   257  		// If any prop is needed we just add all the properties to the result.
   258  		return append(returnFields, "property")
   259  	}
   260  	return returnFields
   261  }
   262  
   263  // If specFile includes sortBy or limit, the produced AQL won't include property in the include section.
   264  // This due to an Artifactory limitation related to using these flags with props in an AQL statement.
   265  // Meaning - the result won't contain properties.
   266  func includePropertiesInAqlForSpec(specFile *ArtifactoryCommonParams) bool {
   267  	return !(len(specFile.SortBy) > 0 || specFile.Limit > 0)
   268  }
   269  
   270  func appendMissingFields(fields []string, defaultFields []string) []string {
   271  	for _, field := range fields {
   272  		if !fileutils.IsStringInSlice(field, defaultFields) {
   273  			defaultFields = append(defaultFields, field)
   274  		}
   275  	}
   276  	return defaultFields
   277  }
   278  
   279  func prepareFieldsForQuery(fields []string) []string {
   280  	// Since a slice is basically a pointer, we don't want to modify the underlying fields array because it might be used again (like in delete service)
   281  	// We will create new slice with the quoted values and will return it.
   282  	var queryFields []string
   283  	for _, val := range fields {
   284  		queryFields = append(queryFields, `"`+val+`"`)
   285  	}
   286  	return queryFields
   287  }
   288  
   289  // Creates an aql query from a spec file.
   290  func BuildQueryFromSpecFile(specFile *ArtifactoryCommonParams, requiredArtifactProps RequiredArtifactProps) string {
   291  	aqlBody := specFile.Aql.ItemsFind
   292  	query := fmt.Sprintf(`items.find(%s)%s`, aqlBody, buildIncludeQueryPart(getQueryReturnFields(specFile, requiredArtifactProps)))
   293  	query = appendSortQueryPart(specFile, query)
   294  	query = appendOffsetQueryPart(specFile, query)
   295  	return appendLimitQueryPart(specFile, query)
   296  }
   297  
   298  func appendOffsetQueryPart(specFile *ArtifactoryCommonParams, query string) string {
   299  	if specFile.Offset > 0 {
   300  		query = fmt.Sprintf(`%s.offset(%s)`, query, strconv.Itoa(specFile.Offset))
   301  	}
   302  	return query
   303  }
   304  
   305  func appendLimitQueryPart(specFile *ArtifactoryCommonParams, query string) string {
   306  	if specFile.Limit > 0 {
   307  		query = fmt.Sprintf(`%s.limit(%s)`, query, strconv.Itoa(specFile.Limit))
   308  	}
   309  	return query
   310  }
   311  
   312  func appendSortQueryPart(specFile *ArtifactoryCommonParams, query string) string {
   313  	if len(specFile.SortBy) > 0 {
   314  		query = fmt.Sprintf(`%s.sort({%s})`, query, buildSortQueryPart(specFile.SortBy, specFile.SortOrder))
   315  	}
   316  	return query
   317  }
   318  
   319  func buildSortQueryPart(sortFields []string, sortOrder string) string {
   320  	if sortOrder == "" {
   321  		sortOrder = "asc"
   322  	}
   323  	return fmt.Sprintf(`"$%s":[%s]`, sortOrder, strings.Join(prepareFieldsForQuery(sortFields), `,`))
   324  }
   325  
   326  func createPropsQuery(aqlBody, propKey, propVal string) string {
   327  	propKeyValQueryPart := buildKeyValQueryPart(propKey, propVal)
   328  	propsQuery :=
   329  		`items.find({` +
   330  			`"$and":[%s,{%s}]` +
   331  			`})%s`
   332  	return fmt.Sprintf(propsQuery, aqlBody, propKeyValQueryPart, buildIncludeQueryPart([]string{"name", "repo", "path", "actual_sha1", "property"}))
   333  }
   334  
   335  func buildIncludeQueryPart(fieldsToInclude []string) string {
   336  	fieldsToInclude = prepareFieldsForQuery(fieldsToInclude)
   337  	return fmt.Sprintf(`.include(%s)`, strings.Join(fieldsToInclude, `,`))
   338  }
   339  
   340  // Optimization - If value is a wildcard pattern, return `{"$match":"value"}`. Otherwise, return `"value"`.
   341  func getAqlValue(val string) string {
   342  	var aqlValuePattern string
   343  	if strings.Contains(val, "*") {
   344  		aqlValuePattern = `{"$match":"%s"}`
   345  	} else {
   346  		aqlValuePattern = `"%s"`
   347  	}
   348  	return fmt.Sprintf(aqlValuePattern, val)
   349  }
   350  
   351  func prepareSourceSearchPattern(pattern, target string, repositoryExists bool) string {
   352  	addWildcardIfNeeded(&pattern, repositoryExists)
   353  	pattern = utils.RemovePlaceholderParentheses(pattern, target)
   354  	return pattern
   355  }
   356  
   357  func addWildcardIfNeeded(pattern *string, repositoryExists bool) {
   358  	if strings.HasSuffix(*pattern, "/") || (*pattern == "" && repositoryExists) {
   359  		*pattern += "*"
   360  	}
   361  }