Skip to content

Commit

Permalink
feat: convert string to mongodb query
Browse files Browse the repository at this point in the history
- convert query into a MongoDB query object
- add optional options to customize the query
- update README
- add github action
  • Loading branch information
ajclopez committed Oct 16, 2023
1 parent 3f4ea36 commit 15505ef
Show file tree
Hide file tree
Showing 18 changed files with 1,234 additions and 2 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Go

on:
push:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21.x'
- name: Install dependencies
run: go get .
- name: Build
run: go build -v ./...
- name: Test & prepare coverage
run: go test -coverprofile=coverage.out
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.out
138 changes: 136 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,136 @@
# mgs
Convert URL query parameters to MongoDB queries
[![MIT License][license-shield]][license-url]

<p align="center">
<h3 align="center">Mongo Golang Search</h3>
<p align="center">
Mongo Golang Search provides a query language to a MongoDB database.
<br />
<a href="https://github.com/ajclopez/mgs#usage"><strong>Explore the docs</strong></a>
<br />
<br />
<a href="https://github.com/ajclopez/mgs/issues">Report Bug</a>
·
<a href="https://github.com/ajclopez/mgs/issues">Request Feature</a>
</p>
</p>

# Mongo Golang Search (mgs)


### Content index

* [What is this?](#what-is-this)
* [Getting Started](#getting-started)
* [Installation](#installation)
* [Usage](#usage)
* [Supported features](#supported-features)
* [Filtering](#filtering)
* [Pagination](#pagination)
* [Sorting](#sorting)
* [Projection](#projection)
* [Available options](#available-options)
* [Customize limit value](#customize-limit-value)
* [Specify casting per param keys](#specify-casting-per-param-keys)
* [Contributing](#contributing)
* [License](#license)

## What is this?

Mongo Golang Search provides a simple query language to perform advanced searches for your collections in **MongoDB**.

You could also use **Mongo Golang Search** to searching, sorting, pagination and combining logical operators.

## Getting Started

### Installation


## Usage


## Supported features

### Filtering

| Operator | URI | Example |
| ----------------- | --------------------- | --------------------------------- |
| `$eq` | `key=val` | `type=public` |
| `$ne` | `key!=val` | `status!=SENT` |
| `$gt` | `key>val` | `price>5` |
| `$gte` | `key>=val` | `price>=9` |
| `$lt` | `key<val` | `date<2020-01-01T14:00:00.000Z` |
| `$lte` | `key<=val` | `priority<=-5` |
| `$in` | `key=val1,val2` | `status=QUEUED,DEQUEUED` |
| `$nin` | `key!=val1,val2` | `status!=QUEUED,DEQUEUED` |
| `$exists` | `key` | `email` |
| `$exists` | `!key` | `!email` |
| `$regex` | `key=/value/<opts>` | `email=/@gmail\.com$/` |
| `$regex` | `key!=/value/<opts>` | `phone!=/^58/` |


### Pagination

Useful to limit the number of records returned.

- Operator keys are `skip` and `limit`.
- Use `limit` operator to limit the number of records returned.
- Use `skip` operator to skip the specified number of records.

```json
skip=20&limit=10
```

### Sorting

Useful to sort returned records.

- Operator key is `sort`.
- It accepts a comma-separated list of fields.
- Use `-` prefixes to sort in descending order.
- Use `+` prefixes to sort in ascedending order.

```json
sort=id,-date
```

### Projection

Useful to limit fields to return in each records.

- Operator key is `fields`.
- It accepts a comma-separated list of fields.

```json
fields=firstname,lastname,phone,email
```

**Note:**
* The `_id` field (returned by default).

## Available options

You can use advanced options:

### Customize limit value


### Specify casting per param keys


## Contributing

Should you like to provide any feedback, please open up an Issue, I appreciate feedback and comments. Any contributions you make are **greatly appreciated**.

1. Fork the Project
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing-feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## License

This software is released under the MIT license. See `LICENSE` for more information.


[license-shield]: https://img.shields.io/badge/License-MIT-yellow.svg
[license-url]: https://github.com/ajclopez/mgs/blob/master/LICENSE
57 changes: 57 additions & 0 deletions convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package mgs

import (
"reflect"
)

// Convert converts the criteria value to a MongoDB query
func Convert(criteria SearchCriteria, filter map[string]interface{}) {

value := ParseValue(criteria.Value, criteria.Caster)

switch criteria.Operation {
case EQUAL:
key := reflect.ValueOf(value).Kind()
if key == reflect.Slice {
filter[criteria.Key] = buildMongoQuery("$in", value)
} else if key == reflect.Struct {
filter[criteria.Key] = buildRegexOperation(value)
} else {
filter[criteria.Key] = value
}
case NOT_EQUAL:
key := reflect.ValueOf(value).Kind()
if key == reflect.Slice {
filter[criteria.Key] = buildMongoQuery("$nin", value)
} else if key == reflect.Struct {
filter[criteria.Key] = buildMongoQuery("$not", buildRegexOperation(value))
} else {
filter[criteria.Key] = buildMongoQuery("$ne", value)
}
case GREATER_THAN:
filter[criteria.Key] = buildMongoQuery("$gt", value)
case GREATER_THAN_EQUAL:
filter[criteria.Key] = buildMongoQuery("$gte", value)
case LESS_THAN:
filter[criteria.Key] = buildMongoQuery("$lt", value)
case LESS_THAN_EQUAL:
filter[criteria.Key] = buildMongoQuery("$lte", value)
case EXISTS:
filter[criteria.Key] = buildMongoQuery("$exists", !criteria.Prefix)
}
}

func buildMongoQuery(operator string, value interface{}) map[string]interface{} {
query := make(map[string]interface{})
query[operator] = value

return query
}

func buildRegexOperation(value interface{}) map[string]interface{} {
regex := make(map[string]interface{})
regex["$regex"] = value.(Regex).Pattern
regex["$options"] = value.(Regex).Option

return regex
}
131 changes: 131 additions & 0 deletions convert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package mgs

import (
"testing"

"github.com/stretchr/testify/assert"
)

var converetTests = []struct {
Criteria SearchCriteria
Filter map[string]interface{}
Expected map[string]interface{}
}{
{
SearchCriteria{
Prefix: false,
Key: "name",
Operation: EQUAL,
Value: "Jhon",
},
map[string]interface{}{},
map[string]interface{}{"name": "Jhon"},
},
{
SearchCriteria{
Prefix: false,
Key: "status",
Operation: EQUAL,
Value: "QUEUED,DEQUEUED",
},
map[string]interface{}{},
map[string]interface{}{"status": map[string]interface{}{"$in": []interface{}{"QUEUED", "DEQUEUED"}}},
},
{
SearchCriteria{
Prefix: false,
Key: "email",
Operation: EQUAL,
Value: "/@gmail\\.com$/",
},
map[string]interface{}{},
map[string]interface{}{"email": map[string]interface{}{"$regex": "@gmail\\.com$", "$options": ""}},
},
{
SearchCriteria{
Prefix: false,
Key: "status",
Operation: NOT_EQUAL,
Value: "SENT",
},
map[string]interface{}{},
map[string]interface{}{"status": map[string]interface{}{"$ne": "SENT"}},
},
{
SearchCriteria{
Prefix: false,
Key: "status",
Operation: NOT_EQUAL,
Value: "QUEUED,DEQUEUED",
},
map[string]interface{}{},
map[string]interface{}{"status": map[string]interface{}{"$nin": []interface{}{"QUEUED", "DEQUEUED"}}},
},
{
SearchCriteria{
Prefix: false,
Key: "phone",
Operation: NOT_EQUAL,
Value: "/^58/",
},
map[string]interface{}{},
map[string]interface{}{"phone": map[string]interface{}{"$not": map[string]interface{}{"$regex": "^58", "$options": ""}}},
},
{
SearchCriteria{
Prefix: false,
Key: "price",
Operation: GREATER_THAN,
Value: "5",
},
map[string]interface{}{},
map[string]interface{}{"price": map[string]interface{}{"$gt": int64(5)}},
},
{
SearchCriteria{
Prefix: false,
Key: "price",
Operation: GREATER_THAN_EQUAL,
Value: "5",
},
map[string]interface{}{},
map[string]interface{}{"price": map[string]interface{}{"$gte": int64(5)}},
},
{
SearchCriteria{
Prefix: false,
Key: "price",
Operation: LESS_THAN,
Value: "5",
},
map[string]interface{}{},
map[string]interface{}{"price": map[string]interface{}{"$lt": int64(5)}},
},
{
SearchCriteria{
Prefix: false,
Key: "price",
Operation: LESS_THAN_EQUAL,
Value: "5",
},
map[string]interface{}{},
map[string]interface{}{"price": map[string]interface{}{"$lte": int64(5)}},
},
{
SearchCriteria{
Prefix: true,
Key: "email",
Operation: EXISTS,
Value: "",
},
map[string]interface{}{},
map[string]interface{}{"email": map[string]interface{}{"$exists": false}},
},
}

func TestShouldConvertFromSearchCriteria(t *testing.T) {
for _, test := range converetTests {
Convert(test.Criteria, test.Filter)
assert.Equal(t, test.Expected, test.Filter)
}
}
Loading

0 comments on commit 15505ef

Please sign in to comment.