diff --git a/.dockerignore b/.dockerignore index 65959251..8ed4299e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,21 +1,25 @@ .editorconfig .git +.gitignore .github sonar-project.properties -AUTHORS.md -CONTRIBUTING.md +*.md LICENSE Makefile NOTICE -README.md arm/ powerpc/ mips/ .golangci.yml -_temp .vscode -node1 -node2 -node3 -.gitignore -changelog.md +go.work +go.work.sum +tools/ +test_e2e/ +!test_e2e/test_api.go +!test_e2e/go.mod +!test_e2e/go.sum +mocks/ +docker/ +**/*_test.go +docs/ \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 31914924..608172f0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,33 +2,38 @@ name: Bug report about: Create a report to help us improve title: '' -labels: Bug +labels: bug --- -**Describe the bug** +## Describe the bug A clear and concise description of what the bug is. -**To Reproduce** +## To Reproduce Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error -**Expected behavior** +## Expected behavior A clear and concise description of what you expected to happen. -**Screenshots** +## Screenshots If applicable, add screenshots to help explain your problem. -**Application (please complete the following information):** +## Application + +Please complete the following information: - badaas version [X.X.X] or commit hash +- go version +- database vendor and version (in case of bugs related with badaas-orm) -**Additional context** +## Additional context Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md new file mode 100644 index 00000000..9835b3a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/discussion.md @@ -0,0 +1,7 @@ +--- +name: Discussion +about: Start a discussion for BaDaaS +title: '' +labels: question +--- + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/user_story.md b/.github/ISSUE_TEMPLATE/user_story.md index ec61755b..f9a6617a 100644 --- a/.github/ISSUE_TEMPLATE/user_story.md +++ b/.github/ISSUE_TEMPLATE/user_story.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest an idea for this project title: '' -labels: User Story, To be verify +labels: enhancement --- ## Description @@ -26,7 +26,7 @@ labels: User Story, To be verify `[Put all others constraints here, like list of acceptances values or other]` -## Resources: +## Resources `[Put all your resources here, like mockups, diagrams or other here]` @@ -37,4 +37,3 @@ labels: User Story, To be verify ## Links `[Only use by the team, to link this feature with epic, technical tasks or bugs]` - diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c2d7558b..210601a1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,5 @@ :information_source: Don't forget to modify the changelog.md before merging this branch. :information_source: Don't forget to modify config files: -- `badaas.example.yml`: the example file. -- `/scripts/e2e/api/ci-conf.yml`: otherwise you will probably break the CI. (*For local testing please use [act](https://github.com/nektos/act)*). \ No newline at end of file + +- `badaas.example.yml`: the example file. diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e382df70..e3154b32 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -5,6 +5,7 @@ on: - main pull_request: types: [opened, synchronize, reopened] + jobs: branch-naming-rules: name: Check branch name @@ -18,6 +19,35 @@ jobs: min_length: 5 max_length: 50 + check-style: + name: Code style + needs: [branch-naming-rules] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-go@v3 + with: + go-version: '^1.18' + cache: true + - name: badaas lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.53.3 + skip-cache: true + skip-pkg-cache: true + skip-build-cache: true + - name: teste2e lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.53.3 + working-directory: test_e2e + args: --config=../.golangci.yml + skip-cache: true + skip-pkg-cache: true + skip-build-cache: true + unit-tests: name: Unit tests needs: [branch-naming-rules] @@ -30,17 +60,29 @@ jobs: with: go-version: '^1.18' cache: true - - name: Run test - run: go test $(go list ./... | sed 1d) -coverprofile=coverage.out -v + - name: Install gotestsum + run: go install gotest.tools/gotestsum@latest + - name: Run unit tests + run: gotestsum --junitfile unit-tests.xml $(go list ./... | grep -v testintegration) -coverpkg=./... -coverprofile=coverage_unit.out - uses: actions/upload-artifact@v3 with: name: coverage - path: coverage.out + path: coverage_unit.out + - name: Test Report + uses: dorny/test-reporter@v1 + if: always() # run this step even if previous steps failed + with: + name: Unit Tests Report # Name of the check run which will be created + path: unit-tests.xml # Path to test results + reporter: java-junit # Format of test results - check-style: - name: Code style - needs: [branch-naming-rules] + integration-tests: + name: Integration tests + needs: [unit-tests] runs-on: ubuntu-latest + strategy: + matrix: + db: [postgresql, cockroachdb, mysql, sqlite, sqlserver] steps: - uses: actions/checkout@v3 with: @@ -49,14 +91,50 @@ jobs: with: go-version: '^1.18' cache: true - - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + - name: Install gotestsum + run: go install gotest.tools/gotestsum@latest + - name: Start containers + run: docker compose -f "docker/${{ matrix.db }}/docker-compose.yml" up -d + if: ${{ matrix.db != 'sqlite' }} + - uses: kanga333/variable-mapper@master + id: export with: - version: latest - skip-cache: true - skip-pkg-cache: true - skip-build-cache: true + key: ${{ matrix.db }} + map: | + { + "postgresql": { + "dialector": "postgres" + }, + "cockroachdb": { + "dialector": "postgres" + }, + "mysql": { + "dialector": "mysql" + }, + "sqlite": { + "dialector": "sqlite" + }, + "sqlserver": { + "dialector": "sqlserver" + } + } + export_to: env + - name: Run test + run: DB=${{ env.dialector }} gotestsum --junitfile integration-tests-${{ matrix.db }}.xml ./testintegration -tags=${{ matrix.db }} -coverpkg=./... -coverprofile=coverage_int_${{ matrix.db }}.out + - name: Test Report + uses: dorny/test-reporter@v1 + if: always() # run this step even if previous steps failed + with: + name: ${{ matrix.db }} Integration Tests Report # Name of the check run which will be created + path: integration-tests-${{ matrix.db }}.xml # Path to test results + reporter: java-junit # Format of test results + - uses: actions/upload-artifact@v3 + with: + name: coverage + path: coverage_int_${{ matrix.db }}.out + - name: Stop containers + if: ${{ matrix.db != 'sqlite' }} + run: docker stop badaas-test-db e2e-tests: name: E2E Tests @@ -71,41 +149,39 @@ jobs: go-version: '^1.18' cache: true - name: Start containers - run: docker compose -f "scripts/e2e/docker-compose.yml" up -d --build + run: docker compose -f "docker/cockroachdb/docker-compose.yml" -f "docker/test_api/docker-compose.yml" up -d --build - name: Wait for API server to be up uses: mydea/action-wait-for-api@v1 with: url: "http://localhost:8000/info" - timeout: 20 + timeout: 60 - name: Run test - run: go test -v + run: go test ./test_e2e -v - name: Get logs if: always() - run: docker compose -f "scripts/e2e/docker-compose.yml" logs --no-color 2>&1 | tee app.log & + run: docker compose -f "docker/cockroachdb/docker-compose.yml" -f "docker/test_api/docker-compose.yml" logs --no-color 2>&1 | tee app.log & - name: Stop containers if: always() - run: docker compose -f "scripts/e2e/docker-compose.yml" down + run: docker compose -f "docker/cockroachdb/docker-compose.yml" -f "docker/test_api/docker-compose.yml" down - uses: actions/upload-artifact@v3 with: name: docker-compose-e2e-logs path: app.log - + sonarcloud: name: SonarCloud - needs: [unit-tests, check-style] + needs: [check-style, integration-tests] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Download line coverage report + - name: Download line coverage reports uses: actions/download-artifact@v3 with: name: coverage - path: coverage.out - name: SonarCloud Scan uses: sonarsource/sonarcloud-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d477eb47..5041efb2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,21 +13,24 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +*-tests*.xml # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ # Go workspace file -go.work +# go.work -# cockroach files +# database files node* +testintegration/sqlite:* -#Vscode conf +# vscode conf .vscode # binary output badaas +!docs/badaas -# temporary directories -_temp \ No newline at end of file +# Sphinx documentation +docs/_build/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index d4705090..9bf38e5e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,242 +1,207 @@ -# WARNING: DO NOT EDIT, THIS FILE IS PROBABLY A COPY -# -# The original version of this file is located in the https://github.com/istio/common-files repo. -# If you're looking at this file in a different repo and want to make a change, please go to the -# common-files repo, make the change there and check it in. Then come back to this repo and run -# "make update-common". - -output: - # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions - # - # Multiple can be specified by separating them by comma, output can be provided - # for each of them by separating format name and path by colon symbol. - # Output path can be either `stdout`, `stderr` or path to the file to write to. - # Example: "checkstyle:report.json,colored-line-number" - # - # Default: colored-line-number - format: github-actions - # Print lines of code with issue. - # Default: true - print-issued-lines: false - # Print linter name in the end of issue text. - # Default: true - print-linter-name: false - # Make issues output unique by line. - # Default: true - uniq-by-line: false - # Add a prefix to the output file references. - # Default is no prefix. - path-prefix: "" - # Sort results by: filepath, line and column. - sort-results: false +# based on +# - https://github.com/istio/common-files/blob/master/files/common/config/.golangci.yml +# - https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 run: - tests: false # timeout for analysis, e.g. 30s, 5m, default is 1m - deadline: 20m - build-tags: - - integ - - integfuzz + deadline: 3m # which dirs to skip: they won't be analyzed; # can use regexp here: generated.*, regexp is applied on full path; # default value is empty list, but next dirs are always skipped independently # from this option's value: - #vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ skip-dirs: - - genfiles$ - - vendor$ - + - mocks$ # which files to skip: they will be analyzed, but issues from them # won't be reported. Default value is empty list, but there is # no need to include all autogenerated files, we confidently recognize - # autogenerated files. If it's not please let us know. - skip-files: - - ".*\\.pb\\.go" - - ".*\\.gen\\.go" + # autogenerated files. + # skip-files: + # - ".*\\.pb\\.go" + # - ".*\\.gen\\.go" linters: - disable-all: true - enable: - - deadcode - - exportloopref - - gocritic - - revive - - gosimple - - govet - - ineffassign - - lll - - misspell - - staticcheck - - structcheck - - stylecheck - - typecheck - - unconvert - - unparam - - varcheck + enable-all: true + disable: + - dogsled # [sometimes necessary] checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + - exhaustruct # [make it easier to construct structs] checks if all structure fields are initialized + - gochecknoglobals # [we use them] checks that no global variables exist + - gochecknoinits # [we use them] checks that no init functions are present in Go code + - godot # [not necessary] checks if comments end in a period + - godox # [we use them] detects FIXME, TODO and other comment keywords + - ireturn # [useful for mocks generation] accept interfaces, return concrete types + - nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + - nonamedreturns # [are util sometimes] reports all named returns + - paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + - testpackage # [doesn't allow white box tests] makes you use a separate _test package + - thelper # [not the expected result by us] detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + # deprecated + - deadcode # [deprecated, replaced by unused] finds unused code + - exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized + - golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes + - ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible + - interfacer # [deprecated] suggests narrower interface types + - maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted + - nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name + - scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs + - structcheck # [deprecated, replaced by unused] finds unused struct fields + - varcheck # [deprecated, replaced by unused] finds unused global variables and constants + # can be util in the future for better errors + - goerr113 # [too strict] checks the errors handling expressions + - wrapcheck # [too strict] checks that errors returned from external packages are wrapped fast: false +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 20 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 0.0 + depguard: + rules: + main: + deny: + - pkg: "github.com/gogo/protobuf" + desc: gogo/protobuf is deprecated, use golang/protobuf + errcheck: + # report about not checking of errors in type assetions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: true + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: false + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 80 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 40 + gci: + sections: + - standard # Captures all standard packages if they do not match another section. + - default # Contains all imports that could not be matched to another section type. + - prefix(github.com/ditrit/) # Groups all imports with the specified Prefix. + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com/ditrit + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/google/uuid + reason: "gofrs' package is not go module" govet: - # report about shadowed variables - check-shadowing: false - maligned: - # print struct with more effective memory layout or not, false by default - suggest-new: true - misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - locale: US - ignore-words: - - cancelled + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: false lll: # max line length, lines longer will be reported. Default is 120. # '\t' is counted as 1 character by default, and can be changed with the tab-width option line-length: 160 # tab width in spaces. Default to 1. tab-width: 1 - revive: - ignore-generated-header: false - severity: "warning" - confidence: 0.0 - error-code: 2 - warning-code: 1 - rules: - - name: blank-imports - - name: context-keys-type - - name: time-naming - - name: var-declaration - - name: unexported-return - - name: errorf - - name: context-as-argument - - name: dot-imports - - name: error-return - - name: error-strings - - name: error-naming - - name: increment-decrement - - name: var-naming - - name: package-comments - - name: range - - name: receiver-naming - - name: indent-error-flow - - name: superfluous-else - - name: modifies-parameter - - name: unreachable-code - - name: struct-tag - - name: constant-logical-expr - - name: bool-literal-in-expr - - name: redefines-builtin-id - - name: imports-blacklist - - name: range-val-in-closure - - name: range-val-address - - name: waitgroup-by-value - - name: atomic - - name: call-to-gc - - name: duplicated-imports - - name: string-of-int - - name: defer - arguments: [["call-chain"]] - - name: unconditional-recursion - - name: identical-branches - # the following rules can be enabled in the future - # - name: empty-lines - # - name: confusing-results - # - name: empty-block - # - name: get-return - # - name: confusing-naming - # - name: unexported-naming - - name: early-return - # - name: unused-parameter - # - name: unnecessary-stmt - # - name: deep-exit - # - name: import-shadowing - # - name: modifies-value-receiver - # - name: unused-receiver - # - name: bare-return - # - name: flag-parameter - # - name: unhandled-error - # - name: if-return - unused: - # treat code as a program (not a library) and report unused exported identifiers; default is false. - # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find funcs usages. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + ignore-words: + - cancelled + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [ funlen, gocognit, lll ] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx unparam: # call graph construction algorithm (cha, rta). In general, use cha for libraries, # and rta for programs with main packages. Default is cha. - algo: rta - + algo: cha # Inspect exported functions, default is false. Set to true if no external program/library imports your code. # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: # if it's called for subdir of a project it can't find external interfaces. All text editor integrations # with golangci-lint call it on a directory with the changed file. check-exported: false - gocritic: - enabled-checks: - - appendCombine - - argOrder - - assignOp - - badCond - - boolExprSimplify - - builtinShadow - - captLocal - - caseOrder - - codegenComment - - commentedOutCode - - commentedOutImport - - defaultCaseOrder - - deprecatedComment - - docStub - - dupArg - - dupBranchBody - - dupCase - - dupSubExpr - - elseif - - emptyFallthrough - - equalFold - - flagDeref - - flagName - - hexLiteral - - indexAlloc - - initClause - - methodExprCall - - nilValReturn - - octalLiteral - - offBy1 - - rangeExprCopy - - regexpMust - - sloppyLen - - stringXbytes - - switchTrue - - typeAssertChain - - typeSwitchVar - - typeUnparen - - underef - - unlambda - - unnecessaryBlock - - unslice - - valSwap - - weakCond - - # Unused - # - yodaStyleExpr - # - appendAssign - # - commentFormatting - # - emptyStringTest - # - exitAfterDefer - # - ifElseChain - # - hugeParam - # - importShadow - # - nestingReduce - # - paramTypeCombine - # - ptrToRefParam - # - rangeValCopy - # - singleCaseSwitch - # - sloppyReassign - # - unlabelStmt - # - unnamedResult - # - wrapperFunc + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find funcs usages. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + gosec: + config: + # Globals are applicable to all rules. + global: + # If true, ignore #nosec in comments (and an alternative as well). + # Default: false + nosec: true + # Audit mode enables addition checks that for normal code analysis might be too nosy. + # Default: false + audit: true issues: # List of regexps of issue texts to exclude, empty list by default. @@ -245,27 +210,29 @@ issues: # excluded by default patterns execute `golangci-lint run --help` exclude: - composite literal uses unkeyed fields - exclude-rules: # Exclude some linters from running on test files. - - path: _test\.go$|^tests/|^samples/ + - path: _test\.go$|^testintegration/|^test_e2e/ linters: - errcheck - - maligned - - # TODO(https://github.com/dominikh/go-tools/issues/732) remove this once we update + - forcetypeassert + - funlen + - goconst + - noctx + - gomnd + # We need to use the deprecated module since the jsonpb replacement is not backwards compatible. - linters: - staticcheck - text: "SA1019: package github.com/golang/protobuf" - + text: "SA1019: package github.com/golang/protobuf/jsonpb" + - linters: + - staticcheck + text: 'SA1019: "github.com/golang/protobuf/jsonpb"' # Independently from option `exclude` we use default exclude patterns, # it can be disabled by this option. To list all # excluded by default patterns execute `golangci-lint run --help`. # Default value for this option is true. exclude-use-default: true - # Maximum issues count per one linter. Set to 0 to disable. Default is 50. max-per-linter: 0 - # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. max-same-issues: 0 \ No newline at end of file diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..7dcd5ee7 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,25 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.10" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +formats: + - pdf + - epub + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..41fe403e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d252da0..3621cc97 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,121 +1,3 @@ -# Contribute to the development of badaas +# Contributing -- [Tests](#tests) - - [Unit tests](#unit-tests) - - [Feature tests (of end to end tests)](#feature-tests-of-end-to-end-tests) -- [Logger](#logger) -- [Directory structure](#directory-structure) -- [Git](#git) - - [Branch naming policy](#branch-naming-policy) - - [Default branch](#default-branch) - - [How to release](#how-to-release) - -## Tests - -### Unit tests - -We use the standard test suite in combination with [github.com/stretchr/testify](https://github.com/stretchr/testify) to do our unit testing. Mocks are generated using [mockery](https://github.com/vektra/mockery) a mock generator using this command `mockery --all --keeptree`. - -To run them, please run: - -```sh -go test $(go list ./... | sed 1d) -v -``` - -### Feature tests (of end to end tests) - -We use docker to run a Badaas instance in combination with one node of CockroachDB. - -Run: - -```sh -docker compose -f "scripts/e2e/docker-compose.yml" up -d --build -``` - -Then in an another shell: - -```sh -go test -v -``` - -The feature files can be found in the `feature` folder. - -## Logger - -We use ubber's [zap](https://pkg.go.dev/go.uber.org/zap) to log stuff, please take `zap.Logger` as an argument for your services constructors. [fx](https://github.com/uber-go/fx) will provide your service with an instance. - -## Directory structure - -This is the directory structure we use for the project: - -- `commands/` *(Go code)*: Contains all the CLI commands. This package relies heavily on github.com/ditrit/verdeter. -- `configuration/` *(Go code)*: Contains all the configuration holders. Please only use the interfaces, they are all mocked for easy testing -- `controllers/` *(Go code)*: Contains all the http controllers, they handle http requests and consume services. -- `docs/`: Contains the documentation. -- `features/`: Contains all the feature tests (or end to end tests). -- `logger/` *(Go code)*: Contains the logger creation logic. Please don't call it from your own services and code, use the dependency injection system. -- `persistance/` *(Go code)*: - - `/gormdatabase/` *(Go code)*: Contains the logic to create a database. Also contains a go package named `gormzap`: it is a compatibility layer between *gorm.io/gorm* and *github.com/uber-go/zap*. - - `/models/` *(Go code)*: Contains the models. (For a structure to me considered a valid model, it has to embed `models.BaseModel` and satisfy the `models.Tabler` interface. This interface returns the name of the sql table.) - - `/dto/` *(Go code)*: Contains the Data Transfert Objects. They are used mainly to decode json payloads. - - `/pagination/` *(Go code)*: Contains the pagination logic. - - `/repository/` *(Go code)*: Contains the repository interface and implementation. Use uint as ID when using gorm models. -- `resources/` *(Go code)*: Contains the resources shared with the rest of the codebase (ex: API version). -- `router/` *(Go code)*: Contains http router of badaas. - - `/middlewares/` *(Go code)*: Contains the various http middlewares that we use. -- `scripts/e2e/` : Contains the docker-compose file for end-to-end test. - - `/api/` : Contains the Dockerfile to build badaas with a dedicated config file. - - `/db/` : Contains the Dockerfile to build a developpement version of CockroachDB. -- `services/` *(Go code)*: Contains the Dockerfile to build a developpement version of CockroachDB. - - `/auth/protocols/`: Contains the implementations of authentication clients for differents protocols. - - `/basicauth/` *(Go code)*: Handle the authentification using email/password. - - `/oidc/` *(Go code)*: Handle the authentication via Open-ID Connect. - - `/sessionservice/` *(Go code)*: Handle sessions and their lifecycle. - - `/userservice/` *(Go code)*: Handle users. - - `validators/` *(Go code)*: Contains validators such as an email validator. - -At the root of the project, you will find: - -- The README. -- The changelog. -- The files for the E2E test http support. -- The LICENCE file. - -## Git - -### Branch naming policy - -`[BRANCH_TYPE]/[BRANCH_NAME]` - -- `BRANCH_TYPE` is a prefix to describe the purpose of the branch. - - Accepted prefixes are: - - `feature`, used for feature development - - `bugfix`, used for bug fix - - `improvement`, used for refacto - - `library`, used for updating library - - `prerelease`, used for preparing the branch for the release - - `release`, used for releasing project - - `hotfix`, used for applying a hotfix on main - - `poc`, used for proof of concept -- `BRANCH_NAME` is managed by this regex: `[a-z0-9._-]` (`_` is used as space character). - -### Default branch - -The default branch is `main`. Direct commit on it is forbidden. The only way to update the application is through pull request. - -Release tag are only done on the `main` branch. - -### How to release - -We use [Semantic Versioning](https://semver.org/spec/v2.0.0.html) as guideline for the version management. - -Steps to release: - -- Create a new branch labeled `release/vX.Y.Z` from the latest `main`. -- Improve the version number in `changelog.md` and `resources/api.go`. -- Verify the content of the `changelog.md`. -- Commit the modifications with the label `Release version X.Y.Z`. -- Create a pull request on github for this branch into `main`. -- Once the pull request validated and merged, tag the `main` branch with `vX.Y.Z` -- After the tag is pushed, make the release on the tag in GitHub +See [this section](./docs/contributing/contributing.md). diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 96dc2afe..00000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# builder image -FROM golang:1.19-alpine as builder -WORKDIR /app -COPY . . -RUN apk add build-base -RUN CGO_ENABLED=1 go build -a -o badaas . - - -# final image for end users -FROM alpine:3.16.2 -COPY --from=builder /app/badaas . -EXPOSE 8000 -ENTRYPOINT [ "./badaas" ] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..7f9a7e4a --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +PATHS = $(shell go list ./... | grep -v testintegration) + +install_dependencies: + go install gotest.tools/gotestsum@latest + go install github.com/vektra/mockery/v2@v2.20.0 + go install github.com/ditrit/badaas-cli@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +lint: + golangci-lint run + cd test_e2e && golangci-lint run --config ../.golangci.yml + +test_unit: + gotestsum --format pkgname $(PATHS) + +rmdb: + docker stop badaas-test-db && docker rm badaas-test-db + +postgresql: + docker compose -f "docker/postgresql/docker-compose.yml" up -d + +cockroachdb: + docker compose -f "docker/cockroachdb/docker-compose.yml" up -d + +mysql: + docker compose -f "docker/mysql/docker-compose.yml" up -d + +sqlserver: + docker compose -f "docker/sqlserver/docker-compose.yml" up -d --build + +test_integration_postgresql: postgresql + DB=postgres gotestsum --format testname ./testintegration + +test_integration_cockroachdb: cockroachdb + DB=postgres gotestsum --format testname ./testintegration -tags=cockroachdb + +test_integration_mysql: mysql + DB=mysql gotestsum --format testname ./testintegration -tags=mysql + +test_integration_sqlite: + DB=sqlite gotestsum --format testname ./testintegration + +test_integration_sqlserver: sqlserver + DB=sqlserver gotestsum --format testname ./testintegration + +test_integration: test_integration_postgresql + +test_e2e: + docker compose -f "docker/cockroachdb/docker-compose.yml" -f "docker/test_api/docker-compose.yml" up -d + ./docker/wait_for_api.sh 8000/info + go test ./test_e2e -v + +test_generate_mocks: + mockery --all --keeptree + +.PHONY: test_unit test_integration test_e2e + diff --git a/README.md b/README.md index 15ade58d..af6ace97 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,44 @@ # BADAAS: Backend And Distribution As A Service -Badaas enables the effortless construction of ***distributed, resilient, highly available and secure applications by design***, while ensuring very simple deployment and management (NoOps). +[![Build Status](https://github.com/ditrit/badaas/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/ditrit/badaas/actions) +[![Go Report Card](https://goreportcard.com/badge/github.com/ditrit/badaas)](https://goreportcard.com/report/github.com/ditrit/badaas) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ditrit_badaas&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ditrit_badaas) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ditrit_badaas&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ditrit_badaas) +[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7624/badge)](https://bestpractices.coreinfrastructure.org/projects/7624) -Badaas provides several key features: +[![Go.Dev reference](https://img.shields.io/badge/go.dev-reference-blue?logo=go&logoColor=white)](https://pkg.go.dev/github.com/ditrit/badaas) -- **Authentification**: Badaas can authentify users using its internal authentification scheme or externally by using protocols such as OIDC, SAML, Oauth2... -- **Habilitation**: On a resource access, Badaas will check if the user is authorized using a RBAC model. -- **Distribution**: Badaas is built to run in clusters by default. Communications between nodes are TLS encrypted using [shoset](https://github.com/ditrit/shoset). -- **Persistence**: Applicative objects are persisted as well as user files. Those resources are shared accross the clusters to increase resiliency. -- **Querying Resources**: Resources are accessible via a REST API. -- **Posix complient**: Badaas strives towards being a good unix citizen and respecting commonly accepted norms. (see [Configuration](#configuration)) -- **Advanced logs management**: Badaas provides an interface to interact with the logs produced by the clusters. Logs are formated in json by default. +[![Discord DitRit](https://dcbadge.vercel.app/api/server/zkKfj9gj2C?style=flat&theme=default-inverted)](https://discord.gg/zkKfj9gj2C) -To quickly get badaas up and running, please head to the [miniblog tutorial]() +BaDaaS enables the effortless construction of ***distributed, resilient, highly available and secure applications by design***, while ensuring very simple deployment and management (NoOps). -- [Quickstart](#quickstart) -- [Docker install](#docker-install) -- [Install from sources](#install-from-sources) - - [Prerequisites](#prerequisites) - - [Configuration](#configuration) -- [Contributing](#contributing) -- [Licence](#licence) +> **Warning** +> BaDaaS is still under development and each of its components can have a different state of evolution -## Quickstart +## Features and components -You can either use the [Docker Install](#docker-install) or build it from source . +Badaas provides several key features, each provided by a component that can be used independently and has a different state of evolution: -## Docker install +- **Authentication**(unstable): Badaas can authenticate users using its internal authentication scheme or externally by using protocols such as OIDC, SAML, Oauth2... +- **Authorization**(wip_unstable): On resource access, Badaas will check if the user is authorized using a RBAC model. +- **Distribution**(todo): Badaas is built to run in clusters by default. Communications between nodes are TLS encrypted using [shoset](https://github.com/ditrit/shoset). +- **Persistence**(wip_unstable): Applicative objects are persisted as well as user files. Those resources are shared across the clusters to increase resiliency. To achieve this, BaDaaS uses the [badaas-orm](https://github.com/ditrit/badaas/orm) component. +- **Querying Resources**(unstable): Resources are accessible via a REST API. +- **Posix compliant**(stable): Badaas strives towards being a good unix citizen and respecting commonly accepted norms. +- **Advanced logs management**(todo): Badaas provides an interface to interact with the logs produced by the clusters. Logs are formatted in json by default. -You can build the image using `docker build -t badaas .` since we don't have an official docker image yet. +## Documentation -## Install from sources + -### Prerequisites - -Get the sources of the project, either by visiting the [releases](https://github.com/ditrit/badaas/releases) page and downloading an archive or clone the main branch (please be aware that is it not a stable version). - -To build the project: - -- [Install go](https://go.dev/dl/#go1.18.4) v1.18 -- Install project dependencies - -```bash -go get -``` - -- Run build command - -```bash -go build . -``` - -Well done, you have a binary `badaas` at the root of the project. - -Then you can launch Badaas directly with: - -```bash -export BADAAS_DATABASE_PORT= -export BADAAS_DATABASE_HOST= -export BADAAS_DATABASE_DBNAME= -export BADAAS_DATABASE_SSLMODE= -export BADAAS_DATABASE_USERNAME= -export BADAAS_DATABASE_PASSWORD= -./badaas -``` - -### Configuration - -Badaas use [verdeter](https://github.com/ditrit/verdeter) to manage it's configuration. So Badaas is POSIX complient by default. - -Badaas can be configured using environment variables, configuration files or CLI flags. -CLI flags take priority on the environment variables and the environment variables take priority on the content of the configuration file. - -As an exemple we will define the `database.port` configuration key using the 3 methods: - -- Using a CLI flag: `--database.port=1222` -- Using an environment variable: `export BADAAS_DATABASE_PORT=1222` (*dots are replaced by underscores*) -- Using a config file (in YAML here): - - ```yml - # /etc/badaas/badaas.yml - database: - port: 1222 - ``` - -The config file can be placed at `/etc/badaas/badaas.yml` or `$HOME/.config/badaas/badaas.yml` or in the same folder as the badaas binary `./badaas.yml`. - -If needed, the location can be overridden using the config key `config_path`. +## Contributing -***For a full overview of the configuration keys: please head to the [configuration documentation](./configuration.md).*** +See [this section](./docs/contributing/contributing.md). -## Contributing +## Code of Conduct -See [this section](./CONTRIBUTING.md). +This project has adopted the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md) -## Licence +## License -Badaas is Licenced under the [Mozilla Public License Version 2.0](./LICENSE). +Badaas is Licensed under the [Mozilla Public License Version 2.0](./LICENSE). diff --git a/badaas.example.yml b/badaas.example.yml index 44313081..ed2bf98c 100644 --- a/badaas.example.yml +++ b/badaas.example.yml @@ -8,6 +8,9 @@ database: # (mandatory) port: 26257 + # The name of the database to use. + name: badaas_db + # The sslmode of the connection to the database server. # (mandatory) sslmode: disable @@ -34,7 +37,7 @@ database: server: # The address to bind badaas to. # default ("0.0.0.0") - host: "" + host: "" # The port badaas should use. # default (8000) @@ -42,7 +45,7 @@ server: # The maximum timeout for the http server in seconds. # default (15) - timeout: 15 + timeout: 15 # The settings for the pagination. pagination: @@ -56,12 +59,35 @@ logger: # Either `dev` or `prod` # default (`prod`) mode: prod + + # Disable error stacktrace from logs + # default (true) + disableStacktrace: true + + # Threshold for the slow query warning in milliseconds + # default (200) + # use 0 to disable slow query warnings + slowQueryThreshold: 200 + + # Threshold for the slow transaction warning in milliseconds + # default (200) + # use 0 to disable slow transaction warnings + slowTransactionThreshold: 200 + + # If true, ignore gorm.ErrRecordNotFound error for logger + # default (false) + ignoreRecordNotFoundError: false + + # If true, don't include params in the query execution logs + # default (false) + parameterizedQueries: false + request: # Change the log emitted when badaas receives a request on a valid endpoint. template: "Receive {{method}} request on {{url}}" # The settings for session service -# This section contains some good defaults, don't change thoses value unless you need to. +# This section contains some good defaults, don't change those value unless you need to. session: # The duration of a user session, in seconds # Default (14400) equal to 4 hours @@ -78,5 +104,4 @@ default: # The admin settings for the first run admin: # The admin password for the first run. Won't change is the admin user already exists. - password: admin - \ No newline at end of file + password: admin \ No newline at end of file diff --git a/badaas.go b/badaas.go index 05513376..925747a2 100644 --- a/badaas.go +++ b/badaas.go @@ -1,11 +1,87 @@ -// Package main : -package main +package badaas import ( - "github.com/ditrit/badaas/commands" + "net/http" + + "github.com/spf13/cobra" + "go.uber.org/fx" + "go.uber.org/fx/fxevent" + "go.uber.org/zap" + + "github.com/ditrit/badaas/configuration" + "github.com/ditrit/badaas/logger" + "github.com/ditrit/badaas/persistence" + "github.com/ditrit/badaas/router" + "github.com/ditrit/verdeter" ) -// Badaas application, run a http-server on 8000. -func main() { - commands.Execute() +var BaDaaS = Initializer{} + +type Initializer struct { + modules []fx.Option +} + +// Allows to select which modules provided by badaas must be added to the application +func (badaas *Initializer) AddModules(modules ...fx.Option) *Initializer { + badaas.modules = append(badaas.modules, modules...) + + return badaas +} + +// Allows to provide constructors to the application +// so that the constructed objects will be available via dependency injection +func (badaas *Initializer) Provide(constructors ...any) *Initializer { + badaas.modules = append(badaas.modules, fx.Provide(constructors...)) + + return badaas +} + +// Allows to invoke functions when the application starts. +// They can take advantage of dependency injection +func (badaas *Initializer) Invoke(funcs ...any) *Initializer { + badaas.modules = append(badaas.modules, fx.Invoke(funcs...)) + + return badaas +} + +// Start the application +func (badaas Initializer) Start() { + rootCommand := verdeter.BuildVerdeterCommand(verdeter.VerdeterConfig{ + Use: "badaas", + Short: "BaDaaS", + Run: badaas.runHTTPServer, + }) + + err := configuration.NewCommandInitializer(configuration.NewKeySetter()).Init(rootCommand) + if err != nil { + panic(err) + } + + rootCommand.Execute() +} + +// Run the http server for badaas +func (badaas Initializer) runHTTPServer(_ *cobra.Command, _ []string) { + modules := []fx.Option{ + // internal modules + configuration.ConfigurationModule, + router.RouterModule, + logger.LoggerModule, + persistence.PersistanceModule, + + // logger for fx + fx.WithLogger(func(logger *zap.Logger) fxevent.Logger { + return &fxevent.ZapLogger{Logger: logger} + }), + + // create httpServer + fx.Provide(newHTTPServer), + // Finally: we invoke the newly created server + fx.Invoke(func(*http.Server) { /* we need this function to be empty*/ }), + } + + fx.New( + // add modules selected by user + append(modules, badaas.modules...)..., + ).Run() } diff --git a/badaas_e2e_test.go b/badaas_e2e_test.go deleted file mode 100644 index 8b5e2072..00000000 --- a/badaas_e2e_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "net/http" - "net/http/cookiejar" - "os" - "testing" - "time" - - "github.com/cucumber/godog" - "github.com/cucumber/godog/colors" - "github.com/spf13/pflag" -) - -type TestContext struct { - statusCode int - json map[string]interface{} - httpClient *http.Client -} - -var opts = godog.Options{Output: colors.Colored(os.Stdout)} - -func init() { - godog.BindCommandLineFlags("godog.", &opts) -} - -func TestMain(m *testing.M) { - pflag.Parse() - opts.Paths = pflag.Args() - - status := godog.TestSuite{ - Name: "godogs", - ScenarioInitializer: InitializeScenario, - Options: &opts, - }.Run() - - os.Exit(status) -} - -func InitializeScenario(ctx *godog.ScenarioContext) { - t := &TestContext{} - jar, err := cookiejar.New(nil) - if err != nil { - panic(err) - } - t.httpClient = &http.Client{ - Transport: http.DefaultTransport, - Timeout: time.Duration(5 * time.Second), - Jar: jar, - } - - ctx.Step(`^I request "(.+)"$`, t.requestGET) - ctx.Step(`^I expect status code is "(\d+)"$`, t.assertStatusCode) - ctx.Step(`^I expect response field "(.+)" is "(.+)"$`, t.assertResponseFieldIsEquals) - ctx.Step(`^I request "(.+)" with method "(.+)" with json$`, t.requestWithJson) -} diff --git a/badaas_test.go b/badaas_test.go new file mode 100644 index 00000000..f8e3568b --- /dev/null +++ b/badaas_test.go @@ -0,0 +1,68 @@ +package badaas + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/mock" + "go.uber.org/fx" + + "github.com/ditrit/badaas/configuration" +) + +func TestInvokeFunctionsWithProvidedValues(_ *testing.T) { + mockObject := mockObject{} + + mockObject.On("Function", 1).Return(1) + + viper.Set(configuration.DatabasePortKey, 5000) + viper.Set(configuration.DatabaseHostKey, "localhost") + viper.Set(configuration.DatabaseUsernameKey, "badaas") + viper.Set(configuration.DatabasePasswordKey, "badaas") + viper.Set(configuration.DatabaseSslmodeKey, "disable") + viper.Set(configuration.DatabaseRetryKey, 0) + + badaas := Initializer{} + badaas.Provide( + newIntValue, + ).Invoke( + mockObject.Function, + shutdown, + ).Start() +} + +func TestAddModulesAreExecuted(_ *testing.T) { + mockObjectI := mockObject{} + + mockObjectI.On("Function", 1).Return(1) + + badaas := Initializer{} + badaas.AddModules( + fx.Module( + "test module", + fx.Provide(newIntValue), + fx.Invoke(mockObjectI.Function), + ), + ).Invoke( + shutdown, + ).Start() +} + +func newIntValue() int { + return 1 +} + +type mockObject struct { + mock.Mock +} + +func (o *mockObject) Function(intValue int) int { + args := o.Called(intValue) + return args.Int(0) +} + +func shutdown( + shutdowner fx.Shutdowner, +) { + shutdowner.Shutdown() +} diff --git a/changelog.md b/changelog.md index 866b2573..7cc819d8 100644 --- a/changelog.md +++ b/changelog.md @@ -9,27 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Setup project (ci and sonar) +- Setup project (ci and sonar). - Setup e2e test solution (cucumber + docker). - Setup Docker based build system. -- Add default api endpoint `info` -- Setup command based pattern using verdeter -- Add an http error handling mecanism -- Add a json controller -- Add a dto package +- Add default api endpoint `info`. +- Setup command based pattern using verdeter. +- Add an http error handling mechanism. +- Add a json controller. +- Add a dto package. - The tasks in the CI are ran in parallel. -- Add a Generic CRUD Repository +- Add a Generic CRUD Repository. - Add a configuration structure containing all the configuration holder. - Refactor codebase to use the DI framework uber-go/fx. Now all services and controllers relies on interfaces. -- Add an generic ID to the repository interface -- Add a retry mecanism for the database connection +- Add an generic ID to the repository interface. +- Add a retry mechanism for the database connection. - Add `init` flag to migrate database and create admin user. - Add a CONTRIBUTING.md and a documentation file for configuration (configuration.md) - Add a session services. -- Add a basic authentification controller. +- Add a basic authentication controller. - Now config keys are only declared once with constants in the `configuration/` package. - Add a dto that is returned on a successful login. -- Update verdeter to version v0.4.0 +- Update verdeter to version v0.4.0. +- Transform BadAas into a library. +- Add badaas-orm with the compilable query system. +- Add operators support +- Add preloading +- Add dynamic and unsafe operators +- Add badaas-orm support for MySQL, SQLServer and SQLite - -[unreleased]: https://github.com/ditrit/badaas/blob/main/changelog.md#unreleased \ No newline at end of file +[unreleased]: https://github.com/ditrit/badaas/blob/main/changelog.md#unreleased diff --git a/commands/initDatabaseCommands.go b/commands/initDatabaseCommands.go deleted file mode 100644 index 022cfb98..00000000 --- a/commands/initDatabaseCommands.go +++ /dev/null @@ -1,32 +0,0 @@ -package commands - -import ( - "github.com/ditrit/badaas/configuration" - "github.com/ditrit/verdeter" -) - -func initDatabaseCommands(cfg *verdeter.VerdeterCommand) { - cfg.GKey(configuration.DatabasePortKey, verdeter.IsInt, "", "The port of the database server") - cfg.SetRequired(configuration.DatabasePortKey) - - cfg.GKey(configuration.DatabaseHostKey, verdeter.IsStr, "", "The host of the database server") - cfg.SetRequired(configuration.DatabaseHostKey) - - cfg.GKey(configuration.DatabaseNameKey, verdeter.IsStr, "", "The name of the database to use") - cfg.SetRequired(configuration.DatabaseNameKey) - - cfg.GKey(configuration.DatabaseUsernameKey, verdeter.IsStr, "", "The username of the account on the database server") - cfg.SetRequired(configuration.DatabaseUsernameKey) - - cfg.GKey(configuration.DatabasePasswordKey, verdeter.IsStr, "", "The password of the account one the database server") - cfg.SetRequired(configuration.DatabasePasswordKey) - - cfg.GKey(configuration.DatabaseSslmodeKey, verdeter.IsStr, "", "The sslmode to use when connecting to the database server") - cfg.SetRequired(configuration.DatabaseSslmodeKey) - - cfg.GKey(configuration.DatabaseRetryKey, verdeter.IsUint, "", "The number of times badaas tries to establish a connection with the database") - cfg.SetDefault(configuration.DatabaseRetryKey, uint(10)) - - cfg.GKey(configuration.DatabaseRetryDurationKey, verdeter.IsUint, "", "The duration in seconds badaas wait between connection attempts") - cfg.SetDefault(configuration.DatabaseRetryDurationKey, uint(5)) -} diff --git a/commands/initInitialisationCommands.go b/commands/initInitialisationCommands.go deleted file mode 100644 index 130e9778..00000000 --- a/commands/initInitialisationCommands.go +++ /dev/null @@ -1,13 +0,0 @@ -package commands - -import ( - "github.com/ditrit/badaas/configuration" - "github.com/ditrit/verdeter" -) - -func initInitialisationCommands(cfg *verdeter.VerdeterCommand) { - - cfg.GKey(configuration.InitializationDefaultAdminPasswordKey, verdeter.IsStr, "", - "Set the default admin password is the admin user is not created yet.") - cfg.SetDefault(configuration.InitializationDefaultAdminPasswordKey, "admin") -} diff --git a/commands/initLoggerCommands.go b/commands/initLoggerCommands.go deleted file mode 100644 index 097ac1d3..00000000 --- a/commands/initLoggerCommands.go +++ /dev/null @@ -1,16 +0,0 @@ -package commands - -import ( - "github.com/ditrit/badaas/configuration" - "github.com/ditrit/verdeter" - "github.com/ditrit/verdeter/validators" -) - -func initLoggerCommands(cfg *verdeter.VerdeterCommand) { - cfg.GKey(configuration.LoggerModeKey, verdeter.IsStr, "", "The logger mode (default to \"prod\")") - cfg.SetDefault(configuration.LoggerModeKey, "prod") - cfg.AddValidator(configuration.LoggerModeKey, validators.AuthorizedValues("prod", "dev")) - - cfg.GKey(configuration.LoggerRequestTemplateKey, verdeter.IsStr, "", "Template message for all request logs") - cfg.SetDefault(configuration.LoggerRequestTemplateKey, "Receive {{method}} request on {{url}}") -} diff --git a/commands/initServerCommands.go b/commands/initServerCommands.go deleted file mode 100644 index 7323524b..00000000 --- a/commands/initServerCommands.go +++ /dev/null @@ -1,23 +0,0 @@ -package commands - -import ( - "github.com/ditrit/badaas/configuration" - "github.com/ditrit/verdeter" - "github.com/ditrit/verdeter/validators" -) - -func initServerCommands(cfg *verdeter.VerdeterCommand) { - cfg.GKey(configuration.ServerTimeoutKey, verdeter.IsInt, "", "Maximum timeout of the http server in second (default is 15s)") - cfg.SetDefault(configuration.ServerTimeoutKey, 15) - - cfg.GKey(configuration.ServerHostKey, verdeter.IsStr, "", "Address to bind (default is 0.0.0.0)") - cfg.SetDefault(configuration.ServerHostKey, "0.0.0.0") - - cfg.GKey(configuration.ServerPortKey, verdeter.IsInt, "p", "Port to bind (default is 8000)") - cfg.AddValidator(configuration.ServerPortKey, validators.CheckTCPHighPort) - cfg.SetDefault(configuration.ServerPortKey, 8000) - - cfg.GKey(configuration.ServerPaginationMaxElemPerPage, verdeter.IsUint, "", "The max number of records returned per page") - cfg.SetDefault(configuration.ServerPaginationMaxElemPerPage, 100) - -} diff --git a/commands/initSessionCommands.go b/commands/initSessionCommands.go deleted file mode 100644 index 13f4ad74..00000000 --- a/commands/initSessionCommands.go +++ /dev/null @@ -1,19 +0,0 @@ -package commands - -import ( - "github.com/ditrit/badaas/configuration" - "github.com/ditrit/verdeter" -) - -// initialize session related config keys -func initSessionCommands(cfg *verdeter.VerdeterCommand) { - cfg.LKey(configuration.SessionDurationKey, verdeter.IsUint, "", "The duration of a user session in seconds.") - cfg.SetDefault(configuration.SessionDurationKey, uint(3600*4)) // 4 hours by default - - cfg.LKey(configuration.SessionPullIntervalKey, - verdeter.IsUint, "", "The refresh interval in seconds. Badaas refresh it's internal session cache periodically.") - cfg.SetDefault(configuration.SessionPullIntervalKey, uint(30)) // 30 seconds by default - - cfg.LKey(configuration.SessionRollIntervalKey, verdeter.IsUint, "", "The interval in which the user can renew it's session by making a request.") - cfg.SetDefault(configuration.SessionRollIntervalKey, uint(3600)) // 1 hour by default -} diff --git a/commands/r.md b/commands/r.md deleted file mode 100644 index c32746b4..00000000 --- a/commands/r.md +++ /dev/null @@ -1,86 +0,0 @@ -package commands - -import ( - "log" - - "github.com/ditrit/badaas/logger" - "github.com/ditrit/badaas/persistence/registry" - "github.com/ditrit/badaas/persistence/repository" - "github.com/ditrit/badaas/router" - "github.com/ditrit/badaas/services/session" - "github.com/ditrit/badaas/services/userservice" - "github.com/ditrit/verdeter" - "go.uber.org/zap" -) - -// Create a super admin user and exit with code 1 on error -func createSuperAdminUser() { - logg := zap.L().Sugar() - _, err := userservice.NewUser("superadmin", "superadmin@badaas.test", "1234") - if err != nil { - if repository.ErrAlreadyExists == err { - logg.Debugf("The superadmin user already exists in database") - } else { - logg.Fatalf("failed to save the super admin %w", err) - } - } - -} - -// Run the http server for badaas -func runHTTPServer(cfg *verdeter.VerdeterCommand, args []string) error { - err := logger.InitLoggerFromConf() - if err != nil { - log.Fatalf("An error happened while initializing logger (ERROR=%s)", err.Error()) - } - - zap.L().Info("The logger is initialiazed") - - // create router - router := router.SetupRouter() - - registryInstance, err := registry.FactoryRegistry(registry.GormDataStore) - if err != nil { - zap.L().Sugar().Fatalf("An error happened while initializing datastorage layer (ERROR=%s)", err.Error()) - } - registry.ReplaceGlobals(registryInstance) - zap.L().Info("The datastorage layer is initialized") - - createSuperAdminUser() - - err = session.Init() - if err != nil { - zap.L().Sugar().Fatalf("An error happened while initializing the session service (ERROR=%s)", err.Error()) - } - zap.L().Info("The session service is initialized") - - // create server - srv := createServerFromConfiguration(router) - - zap.L().Sugar().Infof("Ready to serve at %s\n", srv.Addr) - return srv.ListenAndServe() -} - -var rootCfg = verdeter.NewVerdeterCommand( - "badaas", - "Backend and Distribution as a Service", - `Badaas stands for Backend and Distribution as a Service.`, - runHTTPServer, -) - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - rootCfg.Execute() -} - -func init() { - rootCfg.Initialize() - - rootCfg.GKey("config_path", verdeter.IsStr, "", "Path to the config file/directory") - rootCfg.SetDefault("config_path", ".") - - initServerCommands(rootCfg) - initLoggerCommands(rootCfg) - initDatabaseCommands(rootCfg) -} diff --git a/commands/rootCmd.go b/commands/rootCmd.go deleted file mode 100644 index 605b3bec..00000000 --- a/commands/rootCmd.go +++ /dev/null @@ -1,70 +0,0 @@ -package commands - -import ( - "net/http" - - "github.com/ditrit/badaas/configuration" - "github.com/ditrit/badaas/controllers" - "github.com/ditrit/badaas/logger" - "github.com/ditrit/badaas/persistence" - "github.com/ditrit/badaas/resources" - "github.com/ditrit/badaas/router" - "github.com/ditrit/badaas/services/sessionservice" - "github.com/ditrit/badaas/services/userservice" - "github.com/ditrit/verdeter" - "github.com/spf13/cobra" - "go.uber.org/fx" - "go.uber.org/fx/fxevent" - "go.uber.org/zap" -) - -// Run the http server for badaas -func runHTTPServer(cmd *cobra.Command, args []string) { - fx.New( - // Modules - configuration.ConfigurationModule, - router.RouterModule, - controllers.ControllerModule, - logger.LoggerModule, - persistence.PersistanceModule, - - fx.Provide(userservice.NewUserService), - fx.Provide(sessionservice.NewSessionService), - // logger for fx - fx.WithLogger(func(logger *zap.Logger) fxevent.Logger { - return &fxevent.ZapLogger{Logger: logger} - }), - - fx.Provide(NewHTTPServer), - - // Finally: we invoke the newly created server - fx.Invoke(func(*http.Server) { /* we need this function to be empty*/ }), - fx.Invoke(createSuperUser), - ).Run() -} - -// The command badaas -var rootCfg = verdeter.BuildVerdeterCommand(verdeter.VerdeterConfig{ - Use: "badaas", - Short: "Backend and Distribution as a Service", - Long: "Badaas stands for Backend and Distribution as a Service.", - Version: resources.Version, - Run: runHTTPServer, -}) - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - rootCfg.Execute() -} - -func init() { - rootCfg.GKey("config_path", verdeter.IsStr, "", "Path to the config file/directory") - rootCfg.SetDefault("config_path", ".") - - initServerCommands(rootCfg) - initLoggerCommands(rootCfg) - initDatabaseCommands(rootCfg) - initInitialisationCommands(rootCfg) - initSessionCommands(rootCfg) -} diff --git a/commands/server.go b/commands/server.go deleted file mode 100644 index 1d023b42..00000000 --- a/commands/server.go +++ /dev/null @@ -1,71 +0,0 @@ -package commands - -// This file holds functions needed by the badaas rootCommand, thoses functions help in creating the http.Server. - -import ( - "context" - "fmt" - "net" - "net/http" - "time" - - "go.uber.org/fx" - "go.uber.org/zap" - - "github.com/ditrit/badaas/configuration" -) - -// Create the server from the configuration holder and the http handler -func createServerFromConfigurationHolder(router http.Handler, httpServerConfig configuration.HTTPServerConfiguration) *http.Server { - address := addrFromConf(httpServerConfig.GetHost(), httpServerConfig.GetPort()) - timeout := httpServerConfig.GetMaxTimeout() - return createServer(router, address, timeout, timeout) -} - -// Create an http server -func createServer(router http.Handler, address string, writeTimeout, readTimeout time.Duration) *http.Server { - srv := &http.Server{ - Handler: router, - Addr: address, - - WriteTimeout: writeTimeout, - ReadTimeout: readTimeout, - } - return srv -} - -// Create the addr string for the http.Server -// returns ":" -func addrFromConf(host string, port int) string { - address := fmt.Sprintf("%s:%d", - host, - port, - ) - return address -} - -func NewHTTPServer( - lc fx.Lifecycle, - logger *zap.Logger, - router http.Handler, - httpServerConfig configuration.HTTPServerConfiguration, -) *http.Server { - srv := createServerFromConfigurationHolder(router, httpServerConfig) - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - ln, err := net.Listen("tcp", srv.Addr) - if err != nil { - return err - } - logger.Sugar().Infof("Ready to serve at %s", srv.Addr) - go srv.Serve(ln) - return nil - }, - OnStop: func(ctx context.Context) error { - // Flush the logger - logger.Sync() - return srv.Shutdown(ctx) - }, - }) - return srv -} diff --git a/commands/server_test.go b/commands/server_test.go deleted file mode 100644 index aa949b71..00000000 --- a/commands/server_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package commands - -// This files holds the tests for the commands/server.go file. - -import ( - "net/http" - "testing" - "time" - - "github.com/ditrit/badaas/configuration" - "github.com/stretchr/testify/assert" -) - -func Test_addrFromConf(t *testing.T) { - expected := "192.168.236.222:25100" - addr := addrFromConf("192.168.236.222", 25100) - assert.Equal(t, expected, addr) -} -func Test_createServer(t *testing.T) { - handl := http.NewServeMux() - timeout := time.Duration(time.Second) - srv := createServer( - handl, - "localhost:8000", - timeout, timeout, - ) - assert.NotNil(t, srv) -} - -func TestCreateServerFromConfigurationHolder(t *testing.T) { - handl := http.NewServeMux() - - srv := createServerFromConfigurationHolder(handl, configuration.NewHTTPServerConfiguration()) - assert.NotNil(t, srv) - -} diff --git a/configuration.md b/configuration.md deleted file mode 100644 index 06383628..00000000 --- a/configuration.md +++ /dev/null @@ -1,150 +0,0 @@ -# Configuration - -To see a complete example of a working config file: head to [`badaas.example.yml`](./badaas.example.yml) - -As said in the README: - -> Badaas can be configured using environment variables, CLI flags or a configuration file. -> CLI flags take priority on the environment variables and the environment variables take priority on the content of the configuration file. - -In this documentation file, we will mainly focus our attention on config files but we won't forget that we can use environement variables and CLI flags to change Badaas' config. - -The config file can be formated in any syntax that [github.com/spf13/viper](https://github.com/spf13/viper) supports but we will only use YAML syntax in our docs. - -- [Configuration](#configuration) - - [Database](#database) - - [Logger](#logger) - - [HTTP Server](#http-server) - - [Default values](#default-values) - - [Session management](#session-management) - -## Database - -We use CockroachDB as a database. It is Postgres compatible, so the information we need to provide will not be a surprise to Postgres users. - -```yml -# The settings for the database. -database: - # The host of the database server. - # (mandatory) - host: e2e-db-1 - - # The port of the database server. - # (mandatory) - port: 26257 - - # The sslmode of the connection to the database server. - # (mandatory) - sslmode: disable - - # The username of the account on the database server. - # (mandatory) - username: root - - # The password of the account on the database server. - # (mandatory) - password: postgres - - # The settings for the initialization of the database server. - init: - # Number of time badaas will try to establish a connection to the database server. - # default (10) - retry: 10 - - # Waiting time between connection, in seconds. - # default (5) - retryTime: 5 -``` - -Please note that the init section `init:` is not mandatory. Badaas is suited with a simple but effective retry mecanism that will retry `database.init.retry` time to establish a connection with the database. Badaas will wait `database.init.retryTime` seconds between each retry. - -## Logger - -Badaas use a structured logger that can output json logs in production and user adapted logs for debug using the `logger.mode` key. - -Badaas offers the possibility to change the log message of the Middleware Logger but provides a sane default. It is formated using the Jinja syntax. The values available are `method`, `url` and `protocol`. - -```yml -# The settings for the logger. -logger: - # Either `dev` or `prod` - # default (`prod`) - mode: prod - request: - # Change the log emitted when badaas receives a request on a valid endpoint. - template: "Receive {{method}} request on {{url}}" -``` - -## HTTP Server - -You can change the host Badaas will bind to, the port and the timeout in seconds. - -Additionaly you can change the number of elements returned by default for a paginated response. - -```yml -# The settings for the http server. -server: - # The address to bind badaas to. - # default ("0.0.0.0") - host: "" - - # The port badaas should use. - # default (8000) - port: 8000 - - # The maximum timeout for the http server in seconds. - # default (15) - timeout: 15 - - # The settings for the pagination. - pagination: - page: - # The maximum number of record per page - # default (100) - max: 100 -``` - -## Default values - -The section allow to change some settings for the first run. - -```yml -# The settings for the first run. -default: - # The admin settings for the first run - admin: - # The admin password for the first run. Won't change is the admin user already exists. - password: admin -``` - -## Session management - -You can change the way the session service handle user sessions. -Session are extended if the user made a request to badaas in the "roll duration". The session duration and the refresh interval of the cache can be changed. They contains some good defaults. - -Please see the diagram below to see what is the roll duration relative to the session duration. - - -```txt - | session duration | - |<----------------------------------------->| - ----|-------------------------|-----------------|----> time - | | | - |<--------------->| - roll duration -``` - -```yml -# The settings for session service -# This section contains some good defaults, don't change thoses value unless you need to. -session: - # The duration of a user session, in seconds - # Default (14400) equal to 4 hours - duration: 14400 - # The refresh interval in seconds. Badaas refresh it's internal session cache periodically. - # Default (30) - pullInterval: 30 - # The duration in which the user can renew it's session by making a request. - # Default (3600) equal to 1 hour - rollDuration: 3600 -``` diff --git a/configuration/CommandInitializer.go b/configuration/CommandInitializer.go new file mode 100644 index 00000000..51569748 --- /dev/null +++ b/configuration/CommandInitializer.go @@ -0,0 +1,51 @@ +package configuration + +import ( + "github.com/ditrit/verdeter" +) + +type CommandInitializer interface { + // Inits VerdeterCommand "command" with the all the keys that are configurable in badaas + Init(command *verdeter.VerdeterCommand) error +} + +// Implementation of the CommandInitializer +type commandInitializerImpl struct { + KeySetter KeySetter + Keys []KeyDefinition +} + +// Constructor of CommandInitializer with the keys for badaas +// it uses the keySetter to set the configuration keys in the VerdeterCommand +func NewCommandInitializer(keySetter KeySetter) CommandInitializer { + keys := []KeyDefinition{ + { + Name: "config_path", + ValType: verdeter.IsStr, + Usage: "Path to the config file/directory", + DefaultV: ".", + }, + } + keys = append(keys, getDatabaseConfigurationKeys()...) + keys = append(keys, getSessionConfigurationKeys()...) + keys = append(keys, getInitializationConfigurationKeys()...) + keys = append(keys, getServerConfigurationKeys()...) + keys = append(keys, getLoggerConfigurationKeys()...) + + return commandInitializerImpl{ + KeySetter: keySetter, + Keys: keys, + } +} + +// Inits VerdeterCommand "cmd" with the all the keys in the Keys of the initializer +func (initializer commandInitializerImpl) Init(command *verdeter.VerdeterCommand) error { + for _, key := range initializer.Keys { + err := initializer.KeySetter.Set(command, key) + if err != nil { + return err + } + } + + return nil +} diff --git a/configuration/CommandInitializer_test.go b/configuration/CommandInitializer_test.go new file mode 100644 index 00000000..bbe3e1b4 --- /dev/null +++ b/configuration/CommandInitializer_test.go @@ -0,0 +1,39 @@ +package configuration_test + +import ( + "errors" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/ditrit/badaas/configuration" + configurationMocks "github.com/ditrit/badaas/mocks/configuration" + "github.com/ditrit/verdeter" +) + +var rootCommand = verdeter.BuildVerdeterCommand(verdeter.VerdeterConfig{ + Use: "badaas", + Short: "Backend and Distribution as a Service", + Run: doNothing, +}) + +func doNothing(_ *cobra.Command, _ []string) {} + +func TestInitCommandsInitializerSetsAllKeysWithoutError(t *testing.T) { + err := configuration.NewCommandInitializer( + configuration.NewKeySetter(), + ).Init(rootCommand) + assert.Nil(t, err) +} + +func TestInitCommandsInitializerReturnsErrorWhenErrorOnKeySet(t *testing.T) { + mockKeySetter := configurationMocks.NewKeySetter(t) + mockKeySetter.On("Set", mock.Anything, mock.Anything).Return(errors.New("error setting key")) + + commandInitializer := configuration.NewCommandInitializer(mockKeySetter) + + err := commandInitializer.Init(rootCommand) + assert.ErrorContains(t, err, "error setting key") +} diff --git a/configuration/DatabaseConfiguration.go b/configuration/DatabaseConfiguration.go index 293874a1..47558814 100644 --- a/configuration/DatabaseConfiguration.go +++ b/configuration/DatabaseConfiguration.go @@ -5,6 +5,8 @@ import ( "github.com/spf13/viper" "go.uber.org/zap" + + "github.com/ditrit/badaas/utils" ) // The config keys regarding the database settings @@ -21,7 +23,7 @@ const ( // Hold the configuration values for the database connection type DatabaseConfiguration interface { - ConfigurationHolder + Holder GetPort() int GetHost() string GetDBName() string @@ -48,6 +50,7 @@ type databaseConfigurationImpl struct { func NewDatabaseConfiguration() DatabaseConfiguration { databaseConfiguration := new(databaseConfigurationImpl) databaseConfiguration.Reload() + return databaseConfiguration } @@ -100,7 +103,7 @@ func (databaseConfiguration *databaseConfigurationImpl) GetRetry() uint { // Return the waiting time between the database connections in seconds func (databaseConfiguration *databaseConfigurationImpl) GetRetryTime() time.Duration { - return intToSecond(int(databaseConfiguration.retryTime)) + return utils.IntToSecond(int(databaseConfiguration.retryTime)) } // Log the values provided by the configuration holder diff --git a/configuration/DatabaseConfigurationKeys.go b/configuration/DatabaseConfigurationKeys.go new file mode 100644 index 00000000..15a3b3d0 --- /dev/null +++ b/configuration/DatabaseConfigurationKeys.go @@ -0,0 +1,58 @@ +package configuration + +import ( + "github.com/ditrit/verdeter" +) + +// Definition of database configuration keys +func getDatabaseConfigurationKeys() []KeyDefinition { + return []KeyDefinition{ + { + Name: DatabasePortKey, + ValType: verdeter.IsInt, + Usage: "The port of the database server", + Required: true, + }, + { + Name: DatabaseHostKey, + ValType: verdeter.IsStr, + Usage: "The host of the database server", + Required: true, + }, + { + Name: DatabaseNameKey, + ValType: verdeter.IsStr, + Usage: "The name of the database to use", + }, + { + Name: DatabaseSslmodeKey, + ValType: verdeter.IsStr, + Usage: "The sslmode to use when connecting to the database server", + Required: true, + }, + { + Name: DatabaseUsernameKey, + ValType: verdeter.IsStr, + Usage: "The username of the account on the database server", + Required: true, + }, + { + Name: DatabasePasswordKey, + ValType: verdeter.IsStr, + Usage: "The password of the account one the database server", + Required: true, + }, + { + Name: DatabaseRetryKey, + ValType: verdeter.IsUint, + Usage: "The number of times badaas tries to establish a connection with the database", + DefaultV: defaultDatabaseRetry, + }, + { + Name: DatabaseRetryDurationKey, + ValType: verdeter.IsUint, + Usage: "The duration in seconds badaas wait between connection attempts", + DefaultV: defaultDatabaseRetryDuration, + }, + } +} diff --git a/configuration/DatabaseConfiguration_test.go b/configuration/DatabaseConfiguration_test.go index 21d5447c..5a08a0f2 100644 --- a/configuration/DatabaseConfiguration_test.go +++ b/configuration/DatabaseConfiguration_test.go @@ -5,13 +5,14 @@ import ( "testing" "time" - "github.com/ditrit/badaas/configuration" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" + + "github.com/ditrit/badaas/configuration" ) var databaseConfigurationString = ` @@ -35,6 +36,7 @@ database: func setupViperEnvironment(configurationString string) { viper.Reset() viper.SetConfigType("yaml") + err := viper.ReadConfig(strings.NewReader(configurationString)) if err != nil { panic(err) @@ -43,51 +45,63 @@ func setupViperEnvironment(configurationString string) { func TestDatabaseConfigurationNewDBConfig(t *testing.T) { setupViperEnvironment(databaseConfigurationString) + databaseConfiguration := configuration.NewDatabaseConfiguration() assert.NotNil(t, databaseConfiguration, "the database configuration should not be nil") } func TestDatabaseConfigurationGetPort(t *testing.T) { setupViperEnvironment(databaseConfigurationString) + databaseConfiguration := configuration.NewDatabaseConfiguration() assert.Equal(t, 26257, databaseConfiguration.GetPort(), "should be equals") } func TestDatabaseConfigurationGetHost(t *testing.T) { setupViperEnvironment(databaseConfigurationString) + databaseConfiguration := configuration.NewDatabaseConfiguration() assert.Equal(t, "e2e-db-1", databaseConfiguration.GetHost()) } func TestDatabaseConfigurationGetUsername(t *testing.T) { setupViperEnvironment(databaseConfigurationString) + databaseConfiguration := configuration.NewDatabaseConfiguration() assert.Equal(t, "root", databaseConfiguration.GetUsername()) } + func TestDatabaseConfigurationGetPassword(t *testing.T) { setupViperEnvironment(databaseConfigurationString) + databaseConfiguration := configuration.NewDatabaseConfiguration() assert.Equal(t, "postgres", databaseConfiguration.GetPassword()) } + func TestDatabaseConfigurationGetSSLMode(t *testing.T) { setupViperEnvironment(databaseConfigurationString) + databaseConfiguration := configuration.NewDatabaseConfiguration() assert.Equal(t, "disable", databaseConfiguration.GetSSLMode()) } + func TestDatabaseConfigurationGetDBName(t *testing.T) { setupViperEnvironment(databaseConfigurationString) + databaseConfiguration := configuration.NewDatabaseConfiguration() assert.Equal(t, "badaas_db", databaseConfiguration.GetDBName()) } func TestDatabaseConfigurationGetRetryTime(t *testing.T) { setupViperEnvironment(databaseConfigurationString) + databaseConfiguration := configuration.NewDatabaseConfiguration() - assert.Equal(t, time.Duration(5*time.Second), databaseConfiguration.GetRetryTime()) + assert.Equal(t, 5*time.Second, databaseConfiguration.GetRetryTime()) } func TestDatabaseConfigurationGetRetry(t *testing.T) { setupViperEnvironment(databaseConfigurationString) + databaseConfiguration := configuration.NewDatabaseConfiguration() assert.Equal(t, uint(10), databaseConfiguration.GetRetry()) } diff --git a/configuration/ConfigurationHolder.go b/configuration/Holder.go similarity index 87% rename from configuration/ConfigurationHolder.go rename to configuration/Holder.go index 95f150e4..a749222b 100644 --- a/configuration/ConfigurationHolder.go +++ b/configuration/Holder.go @@ -3,7 +3,7 @@ package configuration import "go.uber.org/zap" // Every configuration holder must implement this interface -type ConfigurationHolder interface { +type Holder interface { // Reload the values provided by the configuration holder Reload() diff --git a/configuration/HttpServerConfiguration.go b/configuration/HttpServerConfiguration.go index 0a00f5f3..a99883aa 100644 --- a/configuration/HttpServerConfiguration.go +++ b/configuration/HttpServerConfiguration.go @@ -1,10 +1,13 @@ package configuration import ( + "fmt" "time" "github.com/spf13/viper" "go.uber.org/zap" + + "github.com/ditrit/badaas/utils" ) // The config keys regarding the http server settings @@ -17,7 +20,8 @@ const ( // Hold the configuration values for the http server type HTTPServerConfiguration interface { - ConfigurationHolder + Holder + GetAddr() string GetHost() string GetPort() int GetMaxTimeout() time.Duration @@ -34,6 +38,7 @@ type hTTPServerConfigurationImpl struct { func NewHTTPServerConfiguration() HTTPServerConfiguration { httpServerConfiguration := new(hTTPServerConfigurationImpl) httpServerConfiguration.Reload() + return httpServerConfiguration } @@ -41,7 +46,7 @@ func NewHTTPServerConfiguration() HTTPServerConfiguration { func (httpServerConfiguration *hTTPServerConfigurationImpl) Reload() { httpServerConfiguration.host = viper.GetString(ServerHostKey) httpServerConfiguration.port = viper.GetInt(ServerPortKey) - httpServerConfiguration.timeout = intToSecond(viper.GetInt(ServerTimeoutKey)) + httpServerConfiguration.timeout = utils.IntToSecond(viper.GetInt(ServerTimeoutKey)) } // Return the host addr @@ -54,7 +59,7 @@ func (httpServerConfiguration *hTTPServerConfigurationImpl) GetPort() int { return httpServerConfiguration.port } -// Return the maximum timout for read and write +// Return the maximum timeout for read and write func (httpServerConfiguration *hTTPServerConfigurationImpl) GetMaxTimeout() time.Duration { return httpServerConfiguration.timeout } @@ -67,3 +72,11 @@ func (httpServerConfiguration *hTTPServerConfigurationImpl) Log(logger *zap.Logg zap.Duration("timeout", httpServerConfiguration.timeout), ) } + +// Create the addr string in format: ":" +func (httpServerConfiguration *hTTPServerConfigurationImpl) GetAddr() string { + return fmt.Sprintf("%s:%d", + httpServerConfiguration.GetHost(), + httpServerConfiguration.GetPort(), + ) +} diff --git a/configuration/HttpServerConfiguration_test.go b/configuration/HttpServerConfiguration_test.go index d50119c5..c793f7b5 100644 --- a/configuration/HttpServerConfiguration_test.go +++ b/configuration/HttpServerConfiguration_test.go @@ -4,12 +4,13 @@ import ( "testing" "time" - "github.com/ditrit/badaas/configuration" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" + + "github.com/ditrit/badaas/configuration" ) var HTTPServerConfigurationString = `server: @@ -19,24 +20,35 @@ var HTTPServerConfigurationString = `server: ` func TestHTTPServerConfigurationNewHttpServerConfiguration(t *testing.T) { - assert.NotNil(t, configuration.NewHTTPServerConfiguration(), "the contructor for HttpServerConfiguration should not return a nil value") + assert.NotNil(t, configuration.NewHTTPServerConfiguration(), "the constructor for HttpServerConfiguration should not return a nil value") } func TestHTTPServerConfigurationGetPort(t *testing.T) { setupViperEnvironment(HTTPServerConfigurationString) - HTTPServerConfiguration := configuration.NewHTTPServerConfiguration() - assert.Equal(t, 8000, HTTPServerConfiguration.GetPort()) + + httpServerConfiguration := configuration.NewHTTPServerConfiguration() + assert.Equal(t, 8000, httpServerConfiguration.GetPort()) } + func TestHTTPServerConfigurationGetHost(t *testing.T) { setupViperEnvironment(HTTPServerConfigurationString) - HTTPServerConfiguration := configuration.NewHTTPServerConfiguration() - assert.Equal(t, "0.0.0.0", HTTPServerConfiguration.GetHost()) + + httpServerConfiguration := configuration.NewHTTPServerConfiguration() + assert.Equal(t, "0.0.0.0", httpServerConfiguration.GetHost()) +} + +func TestHTTPServerConfigurationGetAddr(t *testing.T) { + setupViperEnvironment(HTTPServerConfigurationString) + + httpServerConfiguration := configuration.NewHTTPServerConfiguration() + assert.Equal(t, "0.0.0.0:8000", httpServerConfiguration.GetAddr()) } func TestHTTPServerConfigurationGetMaxTimeout(t *testing.T) { setupViperEnvironment(HTTPServerConfigurationString) - HTTPServerConfiguration := configuration.NewHTTPServerConfiguration() - assert.Equal(t, time.Duration(15*time.Second), HTTPServerConfiguration.GetMaxTimeout()) + + httpServerConfiguration := configuration.NewHTTPServerConfiguration() + assert.Equal(t, 15*time.Second, httpServerConfiguration.GetMaxTimeout()) } func TestHTTPServerConfigurationLog(t *testing.T) { @@ -45,8 +57,8 @@ func TestHTTPServerConfigurationLog(t *testing.T) { observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - HTTPServerConfiguration := configuration.NewHTTPServerConfiguration() - HTTPServerConfiguration.Log(observedLogger) + httpServerConfiguration := configuration.NewHTTPServerConfiguration() + httpServerConfiguration.Log(observedLogger) require.Equal(t, 1, observedLogs.Len()) log := observedLogs.All()[0] @@ -55,6 +67,6 @@ func TestHTTPServerConfigurationLog(t *testing.T) { assert.ElementsMatch(t, []zap.Field{ {Key: "port", Type: zapcore.Int64Type, Integer: 8000}, {Key: "host", Type: zapcore.StringType, String: "0.0.0.0"}, - {Key: "timeout", Type: zapcore.DurationType, Integer: int64(time.Duration(time.Second * 15))}, + {Key: "timeout", Type: zapcore.DurationType, Integer: int64(time.Second * 15)}, }, log.Context) } diff --git a/configuration/InitializationConfiguration.go b/configuration/InitializationConfiguration.go index 8e365d23..ac70938c 100644 --- a/configuration/InitializationConfiguration.go +++ b/configuration/InitializationConfiguration.go @@ -12,7 +12,7 @@ const ( // Hold the configuration values for the initialization type InitializationConfiguration interface { - ConfigurationHolder + Holder GetAdminPassword() string } @@ -25,6 +25,7 @@ type initializationConfigurationIml struct { func NewInitializationConfiguration() InitializationConfiguration { initializationConfiguration := &initializationConfigurationIml{} initializationConfiguration.Reload() + return initializationConfiguration } diff --git a/configuration/InitializationConfigurationKeys.go b/configuration/InitializationConfigurationKeys.go new file mode 100644 index 00000000..7eb896ad --- /dev/null +++ b/configuration/InitializationConfigurationKeys.go @@ -0,0 +1,17 @@ +package configuration + +import ( + "github.com/ditrit/verdeter" +) + +// Definition of initialization configuration keys +func getInitializationConfigurationKeys() []KeyDefinition { + return []KeyDefinition{ + { + Name: InitializationDefaultAdminPasswordKey, + ValType: verdeter.IsStr, + Usage: "Set the default admin password is the admin user is not created yet.", + DefaultV: "admin", + }, + } +} diff --git a/configuration/InitializationConfiguration_test.go b/configuration/InitializationConfiguration_test.go index b80fbd9d..b6ffa776 100644 --- a/configuration/InitializationConfiguration_test.go +++ b/configuration/InitializationConfiguration_test.go @@ -3,12 +3,13 @@ package configuration_test import ( "testing" - "github.com/ditrit/badaas/configuration" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" + + "github.com/ditrit/badaas/configuration" ) var initializationConfigurationString = `default: @@ -16,11 +17,12 @@ var initializationConfigurationString = `default: password: admin` func TestInitializationConfigurationInitializationConfiguration(t *testing.T) { - assert.NotNil(t, configuration.NewInitializationConfiguration(), "the contructor for InitializationConfiguration should not return a nil value") + assert.NotNil(t, configuration.NewInitializationConfiguration(), "the constructor for InitializationConfiguration should not return a nil value") } func TestInitializationConfigurationGetInit(t *testing.T) { setupViperEnvironment(initializationConfigurationString) + initializationConfiguration := configuration.NewInitializationConfiguration() assert.Equal(t, "admin", initializationConfiguration.GetAdminPassword()) } diff --git a/configuration/KeySetter.go b/configuration/KeySetter.go new file mode 100644 index 00000000..d549a014 --- /dev/null +++ b/configuration/KeySetter.go @@ -0,0 +1,47 @@ +package configuration + +import ( + "github.com/ditrit/verdeter" + "github.com/ditrit/verdeter/models" +) + +type KeySetter interface { + // Configures the VerdeterCommand "command" with the information contained in "key" + Set(command *verdeter.VerdeterCommand, key KeyDefinition) error +} + +type KeyDefinition struct { + Name string + ValType models.ConfigType + Usage string + Required bool + DefaultV any + Validator *models.Validator +} + +type keySetterImpl struct{} + +func NewKeySetter() KeySetter { + return keySetterImpl{} +} + +// Configures the VerdeterCommand "command" with the information contained in "key" +func (ks keySetterImpl) Set(command *verdeter.VerdeterCommand, key KeyDefinition) error { + if err := command.GKey(key.Name, key.ValType, "", key.Usage); err != nil { + return err + } + + if key.Required { + command.SetRequired(key.Name) + } + + if key.DefaultV != nil { + command.SetDefault(key.Name, key.DefaultV) + } + + if key.Validator != nil { + command.AddValidator(key.Name, *key.Validator) + } + + return nil +} diff --git a/configuration/LoggerConfiguration.go b/configuration/LoggerConfiguration.go index 133da8c8..63f91614 100644 --- a/configuration/LoggerConfiguration.go +++ b/configuration/LoggerConfiguration.go @@ -1,37 +1,68 @@ package configuration import ( + "time" + "github.com/spf13/viper" "go.uber.org/zap" + "gorm.io/gorm/logger" +) + +const ( + ProductionLogger = "prod" + DevelopmentLogger = "dev" ) // The config keys regarding the logger settings const ( - LoggerModeKey string = "logger.mode" - LoggerRequestTemplateKey string = "logger.request.template" + LoggerModeKey string = "logger.mode" + LoggerDisableStacktraceKey string = "logger.disableStacktrace" + LoggerSlowQueryThresholdKey string = "logger.slowQueryThreshold" + LoggerSlowTransactionThresholdKey string = "logger.slowTransactionThreshold" + LoggerIgnoreRecordNotFoundErrorKey string = "logger.ignoreRecordNotFoundError" + LoggerParameterizedQueriesKey string = "logger.parameterizedQueries" + LoggerRequestTemplateKey string = "logger.request.template" ) // Hold the configuration values for the logger type LoggerConfiguration interface { - ConfigurationHolder + Holder GetMode() string + GetLogLevel() logger.LogLevel + GetDisableStacktrace() bool + GetSlowQueryThreshold() time.Duration + GetSlowTransactionThreshold() time.Duration + GetIgnoreRecordNotFoundError() bool + GetParameterizedQueries() bool GetRequestTemplate() string } // Concrete implementation of the LoggerConfiguration interface type loggerConfigurationImpl struct { - mode, requestTemplate string + mode string + disableStacktrace bool + slowQueryThreshold time.Duration + slowTransactionThreshold time.Duration + ignoreRecordNotFoundError bool + parameterizedQueries bool + requestTemplate string } // Instantiate a new configuration holder for the logger func NewLoggerConfiguration() LoggerConfiguration { loggerConfiguration := new(loggerConfigurationImpl) loggerConfiguration.Reload() + return loggerConfiguration } func (loggerConfiguration *loggerConfigurationImpl) Reload() { loggerConfiguration.mode = viper.GetString(LoggerModeKey) + loggerConfiguration.disableStacktrace = viper.GetBool(LoggerDisableStacktraceKey) + loggerConfiguration.slowQueryThreshold = time.Duration(viper.GetInt(LoggerSlowQueryThresholdKey)) * time.Millisecond + loggerConfiguration.slowTransactionThreshold = time.Duration(viper.GetInt(LoggerSlowTransactionThresholdKey)) * time.Millisecond + loggerConfiguration.ignoreRecordNotFoundError = viper.GetBool(LoggerIgnoreRecordNotFoundErrorKey) + loggerConfiguration.parameterizedQueries = viper.GetBool(LoggerParameterizedQueriesKey) loggerConfiguration.requestTemplate = viper.GetString(LoggerRequestTemplateKey) } @@ -40,6 +71,37 @@ func (loggerConfiguration *loggerConfigurationImpl) GetMode() string { return loggerConfiguration.mode } +func (loggerConfiguration *loggerConfigurationImpl) GetLogLevel() logger.LogLevel { + switch loggerConfiguration.mode { + case ProductionLogger: + return logger.Warn + case DevelopmentLogger: + return logger.Info + default: + return logger.Warn + } +} + +func (loggerConfiguration *loggerConfigurationImpl) GetDisableStacktrace() bool { + return loggerConfiguration.disableStacktrace +} + +func (loggerConfiguration *loggerConfigurationImpl) GetSlowQueryThreshold() time.Duration { + return loggerConfiguration.slowQueryThreshold +} + +func (loggerConfiguration *loggerConfigurationImpl) GetSlowTransactionThreshold() time.Duration { + return loggerConfiguration.slowTransactionThreshold +} + +func (loggerConfiguration *loggerConfigurationImpl) GetIgnoreRecordNotFoundError() bool { + return loggerConfiguration.ignoreRecordNotFoundError +} + +func (loggerConfiguration *loggerConfigurationImpl) GetParameterizedQueries() bool { + return loggerConfiguration.parameterizedQueries +} + // Return the template string for logging request func (loggerConfiguration *loggerConfigurationImpl) GetRequestTemplate() string { return loggerConfiguration.requestTemplate diff --git a/configuration/LoggerConfigurationKeys.go b/configuration/LoggerConfigurationKeys.go new file mode 100644 index 00000000..2e962e2a --- /dev/null +++ b/configuration/LoggerConfigurationKeys.go @@ -0,0 +1,57 @@ +package configuration + +import ( + "github.com/ditrit/verdeter" + "github.com/ditrit/verdeter/validators" +) + +// Definition of logger configuration keys +func getLoggerConfigurationKeys() []KeyDefinition { + modeValidator := validators.AuthorizedValues("prod", "dev") + + return []KeyDefinition{ + { + Name: LoggerRequestTemplateKey, + ValType: verdeter.IsStr, + Usage: "Template message for all request logs", + DefaultV: "Receive {{method}} request on {{url}}", + }, + { + Name: LoggerModeKey, + ValType: verdeter.IsStr, + Usage: "The logger mode (default to \"prod\")", + DefaultV: "prod", + Validator: &modeValidator, + }, + { + Name: LoggerDisableStacktraceKey, + ValType: verdeter.IsBool, + Usage: "Disable error stacktrace from logs (default to true)", + DefaultV: true, + }, + { + Name: LoggerSlowQueryThresholdKey, + ValType: verdeter.IsInt, + Usage: "Threshold for the slow query warning in milliseconds (default to 200)", + DefaultV: defaultLoggerSlowQueryThreshold, + }, + { + Name: LoggerSlowTransactionThresholdKey, + ValType: verdeter.IsInt, + Usage: "Threshold for the slow transaction warning in milliseconds (default to 200)", + DefaultV: defaultLoggerSlowTransactionThreshold, + }, + { + Name: LoggerIgnoreRecordNotFoundErrorKey, + ValType: verdeter.IsBool, + Usage: "If true, ignore gorm.ErrRecordNotFound error for logger (default to false)", + DefaultV: false, + }, + { + Name: LoggerParameterizedQueriesKey, + ValType: verdeter.IsBool, + Usage: "If true, don't include params in the query execution logs (default to false)", + DefaultV: false, + }, + } +} diff --git a/configuration/LoggerConfiguration_test.go b/configuration/LoggerConfiguration_test.go index 53b005bb..ca97bf78 100644 --- a/configuration/LoggerConfiguration_test.go +++ b/configuration/LoggerConfiguration_test.go @@ -3,12 +3,13 @@ package configuration_test import ( "testing" - "github.com/ditrit/badaas/configuration" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" + + "github.com/ditrit/badaas/configuration" ) var LoggerConfigurationString = `logger: @@ -18,19 +19,21 @@ var LoggerConfigurationString = `logger: ` func TestLoggerConfigurationNewLoggerConfiguration(t *testing.T) { - assert.NotNil(t, configuration.NewLoggerConfiguration(), "the contructor for LoggerConfiguration should not return a nil value") + assert.NotNil(t, configuration.NewLoggerConfiguration(), "the constructor for LoggerConfiguration should not return a nil value") } func TestLoggerConfigurationLoggerGetMode(t *testing.T) { setupViperEnvironment(LoggerConfigurationString) - LoggerConfiguration := configuration.NewLoggerConfiguration() - assert.Equal(t, "prod", LoggerConfiguration.GetMode()) + + loggerConfiguration := configuration.NewLoggerConfiguration() + assert.Equal(t, "prod", loggerConfiguration.GetMode()) } func TestLoggerConfigurationLoggerRequestTemplate(t *testing.T) { setupViperEnvironment(LoggerConfigurationString) - LoggerConfiguration := configuration.NewLoggerConfiguration() - assert.Equal(t, "{proto} {method} {url}", LoggerConfiguration.GetRequestTemplate()) + + loggerConfiguration := configuration.NewLoggerConfiguration() + assert.Equal(t, "{proto} {method} {url}", loggerConfiguration.GetRequestTemplate()) } func TestLoggerConfigurationLog(t *testing.T) { @@ -39,8 +42,8 @@ func TestLoggerConfigurationLog(t *testing.T) { observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - LoggerConfiguration := configuration.NewLoggerConfiguration() - LoggerConfiguration.Log(observedLogger) + loggerConfiguration := configuration.NewLoggerConfiguration() + loggerConfiguration.Log(observedLogger) require.Equal(t, 1, observedLogs.Len()) log := observedLogs.All()[0] diff --git a/configuration/PaginationConfiguration.go b/configuration/PaginationConfiguration.go index 264a8233..f91408d8 100644 --- a/configuration/PaginationConfiguration.go +++ b/configuration/PaginationConfiguration.go @@ -7,7 +7,7 @@ import ( // Hold the configuration values for the pagination type PaginationConfiguration interface { - ConfigurationHolder + Holder GetMaxElemPerPage() uint } @@ -20,6 +20,7 @@ type paginationConfigurationImpl struct { func NewPaginationConfiguration() PaginationConfiguration { paginationConfiguration := new(paginationConfigurationImpl) paginationConfiguration.Reload() + return paginationConfiguration } diff --git a/configuration/PaginationConfiguration_test.go b/configuration/PaginationConfiguration_test.go index aaca2116..b941b247 100644 --- a/configuration/PaginationConfiguration_test.go +++ b/configuration/PaginationConfiguration_test.go @@ -3,24 +3,26 @@ package configuration_test import ( "testing" - "github.com/ditrit/badaas/configuration" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" + + "github.com/ditrit/badaas/configuration" ) var PaginationConfigurationString = `server.pagination.page.max: 12` func TestPaginationConfigurationNewPaginationConfiguration(t *testing.T) { - assert.NotNil(t, configuration.NewPaginationConfiguration(), "the contructor for PaginationConfiguration should not return a nil value") + assert.NotNil(t, configuration.NewPaginationConfiguration(), "the constructor for PaginationConfiguration should not return a nil value") } func TestPaginationConfigurationGetMaxElemPerPage(t *testing.T) { setupViperEnvironment(PaginationConfigurationString) - PaginationConfiguration := configuration.NewPaginationConfiguration() - assert.Equal(t, uint(12), PaginationConfiguration.GetMaxElemPerPage()) + + paginationConfiguration := configuration.NewPaginationConfiguration() + assert.Equal(t, uint(12), paginationConfiguration.GetMaxElemPerPage()) } func TestPaginationConfigurationLog(t *testing.T) { @@ -29,8 +31,8 @@ func TestPaginationConfigurationLog(t *testing.T) { observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - PaginationConfiguration := configuration.NewPaginationConfiguration() - PaginationConfiguration.Log(observedLogger) + paginationConfiguration := configuration.NewPaginationConfiguration() + paginationConfiguration.Log(observedLogger) require.Equal(t, 1, observedLogs.Len()) log := observedLogs.All()[0] diff --git a/configuration/ServerConfigurationKeys.go b/configuration/ServerConfigurationKeys.go new file mode 100644 index 00000000..869fccf5 --- /dev/null +++ b/configuration/ServerConfigurationKeys.go @@ -0,0 +1,37 @@ +package configuration + +import ( + "github.com/ditrit/verdeter" + "github.com/ditrit/verdeter/validators" +) + +// Definition of server configuration keys +func getServerConfigurationKeys() []KeyDefinition { + return []KeyDefinition{ + { + Name: ServerTimeoutKey, + ValType: verdeter.IsInt, + Usage: "Maximum timeout of the http server in second (default is 15s)", + DefaultV: defaultServerTimeout, + }, + { + Name: ServerHostKey, + ValType: verdeter.IsStr, + Usage: "Address to bind (default is 0.0.0.0)", + DefaultV: defaultServerAddress, + }, + { + Name: ServerPortKey, + ValType: verdeter.IsInt, + Usage: "Port to bind (default is 8000)", + DefaultV: defaultServerPort, + Validator: &validators.CheckTCPHighPort, + }, + { + Name: ServerPaginationMaxElemPerPage, + ValType: verdeter.IsUint, + Usage: "The max number of records returned per page", + DefaultV: defaultServerPaginationMaxElemPerPage, + }, + } +} diff --git a/configuration/SessionConfiguration.go b/configuration/SessionConfiguration.go index 3a76ef6f..242a7739 100644 --- a/configuration/SessionConfiguration.go +++ b/configuration/SessionConfiguration.go @@ -5,6 +5,8 @@ import ( "github.com/spf13/viper" "go.uber.org/zap" + + "github.com/ditrit/badaas/utils" ) // The config keys regarding the session handling settings @@ -16,7 +18,7 @@ const ( // Hold the configuration values to handle the sessions type SessionConfiguration interface { - ConfigurationHolder + Holder GetSessionDuration() time.Duration GetPullInterval() time.Duration GetRollDuration() time.Duration @@ -33,6 +35,7 @@ type sessionConfigurationImpl struct { func NewSessionConfiguration() SessionConfiguration { sessionConfiguration := new(sessionConfigurationImpl) sessionConfiguration.Reload() + return sessionConfiguration } @@ -53,9 +56,9 @@ func (sessionConfiguration *sessionConfigurationImpl) GetRollDuration() time.Dur // Reload session configuration func (sessionConfiguration *sessionConfigurationImpl) Reload() { - sessionConfiguration.sessionDuration = intToSecond(int(viper.GetUint(SessionDurationKey))) - sessionConfiguration.pullInterval = intToSecond(int(viper.GetUint(SessionPullIntervalKey))) - sessionConfiguration.rollDuration = intToSecond(int(viper.GetUint(SessionRollIntervalKey))) + sessionConfiguration.sessionDuration = utils.IntToSecond(int(viper.GetUint(SessionDurationKey))) + sessionConfiguration.pullInterval = utils.IntToSecond(int(viper.GetUint(SessionPullIntervalKey))) + sessionConfiguration.rollDuration = utils.IntToSecond(int(viper.GetUint(SessionRollIntervalKey))) } // Log the values provided by the configuration holder diff --git a/configuration/SessionConfigurationKeys.go b/configuration/SessionConfigurationKeys.go new file mode 100644 index 00000000..7387c816 --- /dev/null +++ b/configuration/SessionConfigurationKeys.go @@ -0,0 +1,29 @@ +package configuration + +import ( + "github.com/ditrit/verdeter" +) + +// Definition of session configuration keys +func getSessionConfigurationKeys() []KeyDefinition { + return []KeyDefinition{ + { + Name: SessionDurationKey, + ValType: verdeter.IsUint, + Usage: "The duration of a user session in seconds", + DefaultV: defaultSessionDuration, + }, + { + Name: SessionPullIntervalKey, + ValType: verdeter.IsUint, + Usage: "The refresh interval in seconds. Badaas refresh it's internal session cache periodically", + DefaultV: defaultSessionPullInterval, + }, + { + Name: SessionRollIntervalKey, + ValType: verdeter.IsUint, + Usage: "The interval in which the user can renew it's session by making a request", + DefaultV: defaultSessionRollInterval, + }, + } +} diff --git a/configuration/SessionConfiguration_test.go b/configuration/SessionConfiguration_test.go index 05a9e101..1d06f183 100644 --- a/configuration/SessionConfiguration_test.go +++ b/configuration/SessionConfiguration_test.go @@ -4,12 +4,13 @@ import ( "testing" "time" - "github.com/ditrit/badaas/configuration" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" + + "github.com/ditrit/badaas/configuration" ) var SessionConfigurationString = `session: @@ -18,25 +19,28 @@ var SessionConfigurationString = `session: rollDuration: 10 # 10 seconds` func TestSessionConfigurationNewSessionConfiguration(t *testing.T) { - assert.NotNil(t, configuration.NewSessionConfiguration(), "the contructor for PaginationConfiguration should not return a nil value") + assert.NotNil(t, configuration.NewSessionConfiguration(), "the constructor for PaginationConfiguration should not return a nil value") } func TestSessionConfigurationGetSessionDuration(t *testing.T) { setupViperEnvironment(SessionConfigurationString) - SessionConfiguration := configuration.NewSessionConfiguration() - assert.Equal(t, time.Duration(time.Hour), SessionConfiguration.GetSessionDuration()) + + sessionConfiguration := configuration.NewSessionConfiguration() + assert.Equal(t, time.Hour, sessionConfiguration.GetSessionDuration()) } -func TestSessionConfigurationGetPullIntervall(t *testing.T) { +func TestSessionConfigurationGetPullInterval(t *testing.T) { setupViperEnvironment(SessionConfigurationString) - SessionConfiguration := configuration.NewSessionConfiguration() - assert.Equal(t, time.Duration(time.Second*30), SessionConfiguration.GetPullInterval()) + + sessionConfiguration := configuration.NewSessionConfiguration() + assert.Equal(t, time.Second*30, sessionConfiguration.GetPullInterval()) } -func TestSessionConfigurationGetRollIntervall(t *testing.T) { +func TestSessionConfigurationGetRollInterval(t *testing.T) { setupViperEnvironment(SessionConfigurationString) - SessionConfiguration := configuration.NewSessionConfiguration() - assert.Equal(t, time.Duration(time.Second*10), SessionConfiguration.GetRollDuration()) + + sessionConfiguration := configuration.NewSessionConfiguration() + assert.Equal(t, time.Second*10, sessionConfiguration.GetRollDuration()) } func TestSessionConfigurationLog(t *testing.T) { @@ -45,16 +49,16 @@ func TestSessionConfigurationLog(t *testing.T) { observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - PaginationConfiguration := configuration.NewSessionConfiguration() - PaginationConfiguration.Log(observedLogger) + paginationConfiguration := configuration.NewSessionConfiguration() + paginationConfiguration.Log(observedLogger) require.Equal(t, 1, observedLogs.Len()) log := observedLogs.All()[0] assert.Equal(t, "Session configuration", log.Message) require.Len(t, log.Context, 3) assert.ElementsMatch(t, []zap.Field{ - {Key: "sessionDuration", Type: zapcore.DurationType, Integer: int64(time.Duration(time.Hour))}, - {Key: "pullInterval", Type: zapcore.DurationType, Integer: int64(time.Duration(time.Second * 30))}, - {Key: "rollDuration", Type: zapcore.DurationType, Integer: int64(time.Duration(time.Second * 10))}, + {Key: "sessionDuration", Type: zapcore.DurationType, Integer: int64(time.Hour)}, + {Key: "pullInterval", Type: zapcore.DurationType, Integer: int64(time.Second * 30)}, + {Key: "rollDuration", Type: zapcore.DurationType, Integer: int64(time.Second * 10)}, }, log.Context) } diff --git a/configuration/defaults.go b/configuration/defaults.go new file mode 100644 index 00000000..fc89caf5 --- /dev/null +++ b/configuration/defaults.go @@ -0,0 +1,15 @@ +package configuration + +const ( + defaultDatabaseRetry = uint(10) + defaultDatabaseRetryDuration = uint(5) + defaultServerTimeout = 15 + defaultServerAddress = "0.0.0.0" + defaultServerPort = 8000 + defaultServerPaginationMaxElemPerPage = uint(100) + defaultSessionDuration = uint(3600 * 4) // 4 hours + defaultSessionPullInterval = uint(30) // 30 seconds + defaultSessionRollInterval = uint(3600) // 1 hour + defaultLoggerSlowQueryThreshold = 200 // milliseconds + defaultLoggerSlowTransactionThreshold = 200 // milliseconds +) diff --git a/controllers/ModuleFx.go b/controllers/ModuleFx.go deleted file mode 100644 index ccab54eb..00000000 --- a/controllers/ModuleFx.go +++ /dev/null @@ -1,10 +0,0 @@ -package controllers - -import "go.uber.org/fx" - -// ControllerModule for fx -var ControllerModule = fx.Module( - "controllers", - fx.Provide(NewInfoController), - fx.Provide(NewBasicAuthentificationController), -) diff --git a/controllers/basicAuth.go b/controllers/basicAuth.go index d98c4de5..b4c3808e 100644 --- a/controllers/basicAuth.go +++ b/controllers/basicAuth.go @@ -2,48 +2,52 @@ package controllers import ( "encoding/json" + "errors" + "fmt" "net/http" + "time" + + "go.uber.org/zap" "github.com/ditrit/badaas/httperrors" + badaasORMErrors "github.com/ditrit/badaas/orm/errors" "github.com/ditrit/badaas/persistence/models/dto" "github.com/ditrit/badaas/services/sessionservice" "github.com/ditrit/badaas/services/userservice" - "go.uber.org/zap" ) -var ( - // Sent when the request is malformed - HTTPErrRequestMalformed httperrors.HTTPError = httperrors.NewHTTPError( - http.StatusBadRequest, - "Request malformed", - "The schema of the received data is not correct", - nil, - false) +const accessTokenCookieExpirationTime = 48 * time.Hour + +// HTTPErrRequestMalformed is sent when the request is malformed +var HTTPErrRequestMalformed httperrors.HTTPError = httperrors.NewHTTPError( + http.StatusBadRequest, + "Request malformed", + "The schema of the received data is not correct", + nil, + false, ) -// Basic Authentification Controller -type BasicAuthentificationController interface { +type BasicAuthenticationController interface { BasicLoginHandler(http.ResponseWriter, *http.Request) (any, httperrors.HTTPError) Logout(http.ResponseWriter, *http.Request) (any, httperrors.HTTPError) } // Check interface compliance -var _ BasicAuthentificationController = (*basicAuthentificationController)(nil) +var _ BasicAuthenticationController = (*basicAuthenticationController)(nil) -// BasicAuthentificationController implementation -type basicAuthentificationController struct { +type basicAuthenticationController struct { logger *zap.Logger userService userservice.UserService sessionService sessionservice.SessionService } -// BasicAuthentificationController contructor -func NewBasicAuthentificationController( +// BasicAuthenticationController constructor +func NewBasicAuthenticationController( logger *zap.Logger, userService userservice.UserService, sessionService sessionservice.SessionService, -) BasicAuthentificationController { - return &basicAuthentificationController{ +) BasicAuthenticationController { + return &basicAuthenticationController{ logger: logger, userService: userService, sessionService: sessionService, @@ -51,25 +55,42 @@ func NewBasicAuthentificationController( } // Log In with username and password -func (basicAuthController *basicAuthentificationController) BasicLoginHandler(w http.ResponseWriter, r *http.Request) (any, httperrors.HTTPError) { +func (basicAuthController *basicAuthenticationController) BasicLoginHandler(w http.ResponseWriter, r *http.Request) (any, httperrors.HTTPError) { var loginJSONStruct dto.UserLoginDTO + err := json.NewDecoder(r.Body).Decode(&loginJSONStruct) if err != nil { return nil, HTTPErrRequestMalformed } - user, herr := basicAuthController.userService.GetUser(loginJSONStruct) - if herr != nil { - return nil, herr + + user, err := basicAuthController.userService.GetUser(loginJSONStruct) + if err != nil { + if errors.Is(err, badaasORMErrors.ErrObjectNotFound) { + return nil, httperrors.NewErrorNotFound( + "user", + fmt.Sprintf("no user found with email %q", loginJSONStruct.Email), + ) + } else if errors.Is(err, userservice.ErrWrongPassword) { + return nil, httperrors.NewUnauthorizedError( + "wrong password", "the provided password is incorrect", + ) + } + + return nil, httperrors.NewDBError(err) } // On valid password, generate a session and return it's uuid to the client - herr = basicAuthController.sessionService.LogUserIn(user, w) + session, err := basicAuthController.sessionService.LogUserIn(user) + if err != nil { + return nil, httperrors.NewDBError(err) + } + + herr := createAndSetAccessTokenCookie(w, session.ID.String()) if herr != nil { return nil, herr - } - return dto.DTOLoginSuccess{ + return dto.LoginSuccess{ Email: user.Email, ID: user.ID.String(), Username: user.Username, @@ -77,7 +98,37 @@ func (basicAuthController *basicAuthentificationController) BasicLoginHandler(w } // Log Out the user -func (basicAuthController *basicAuthentificationController) Logout(w http.ResponseWriter, r *http.Request) (any, httperrors.HTTPError) { - basicAuthController.sessionService.LogUserOut(sessionservice.GetSessionClaimsFromContext(r.Context()), w) +func (basicAuthController *basicAuthenticationController) Logout(w http.ResponseWriter, r *http.Request) (any, httperrors.HTTPError) { + herr := basicAuthController.sessionService.LogUserOut(sessionservice.GetSessionClaimsFromContext(r.Context())) + if herr != nil { + return nil, herr + } + + herr = createAndSetAccessTokenCookie(w, "") + if herr != nil { + return nil, herr + } + return nil, nil } + +func createAndSetAccessTokenCookie(w http.ResponseWriter, sessionUUID string) httperrors.HTTPError { + accessToken := &http.Cookie{ + Name: "access_token", + Path: "/", + Value: sessionUUID, + HttpOnly: true, + SameSite: http.SameSiteNoneMode, // TODO change to http.SameSiteStrictMode in prod + Secure: false, // TODO change to true in prod + Expires: time.Now().Add(accessTokenCookieExpirationTime), + } + + err := accessToken.Valid() + if err != nil { + return httperrors.NewInternalServerError("access token error", "unable to create access token", err) + } + + http.SetCookie(w, accessToken) + + return nil +} diff --git a/controllers/basicAuth_test.go b/controllers/basicAuth_test.go index 2fecf795..f1f3dc6d 100644 --- a/controllers/basicAuth_test.go +++ b/controllers/basicAuth_test.go @@ -1,20 +1,23 @@ package controllers_test import ( + "net/http" "net/http/httptest" "strings" "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" "github.com/ditrit/badaas/controllers" "github.com/ditrit/badaas/httperrors" mocksSessionService "github.com/ditrit/badaas/mocks/services/sessionservice" mocksUserService "github.com/ditrit/badaas/mocks/services/userservice" + "github.com/ditrit/badaas/orm/model" "github.com/ditrit/badaas/persistence/models" "github.com/ditrit/badaas/persistence/models/dto" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "go.uber.org/zap" - "go.uber.org/zap/zaptest/observer" ) func Test_BasicLoginHandler_MalformedRequest(t *testing.T) { @@ -24,22 +27,21 @@ func Test_BasicLoginHandler_MalformedRequest(t *testing.T) { userService := mocksUserService.NewUserService(t) sessionService := mocksSessionService.NewSessionService(t) - controller := controllers.NewBasicAuthentificationController( + controller := controllers.NewBasicAuthenticationController( logger, userService, sessionService, ) response := httptest.NewRecorder() request := httptest.NewRequest( - "POST", - "/v1/auth/basic/login", + http.MethodPost, + "/login", strings.NewReader("qsdqsdqsd"), ) payload, err := controller.BasicLoginHandler(response, request) assert.Equal(t, controllers.HTTPErrRequestMalformed, err) assert.Nil(t, payload) - } func Test_BasicLoginHandler_UserNotFound(t *testing.T) { @@ -52,18 +54,19 @@ func Test_BasicLoginHandler_UserNotFound(t *testing.T) { userService := mocksUserService.NewUserService(t) userService. On("GetUser", loginJSONStruct). - Return(nil, httperrors.AnError) + Return(nil, httperrors.ErrForTests) + sessionService := mocksSessionService.NewSessionService(t) - controller := controllers.NewBasicAuthentificationController( + controller := controllers.NewBasicAuthenticationController( logger, userService, sessionService, ) response := httptest.NewRecorder() request := httptest.NewRequest( - "POST", - "/v1/auth/basic/login", + http.MethodPost, + "/login", strings.NewReader(`{ "email": "bob@email.com", "password":"1234" @@ -73,7 +76,6 @@ func Test_BasicLoginHandler_UserNotFound(t *testing.T) { payload, err := controller.BasicLoginHandler(response, request) assert.Error(t, err) assert.Nil(t, payload) - } func Test_BasicLoginHandler_LoginFailed(t *testing.T) { @@ -85,8 +87,8 @@ func Test_BasicLoginHandler_LoginFailed(t *testing.T) { } response := httptest.NewRecorder() request := httptest.NewRequest( - "POST", - "/v1/auth/basic/login", + http.MethodPost, + "/login", strings.NewReader(`{ "email": "bob@email.com", "password":"1234" @@ -94,7 +96,7 @@ func Test_BasicLoginHandler_LoginFailed(t *testing.T) { ) userService := mocksUserService.NewUserService(t) user := &models.User{ - BaseModel: models.BaseModel{}, + UUIDModel: model.UUIDModel{}, Username: "bob", Email: "bob@email.com", Password: []byte("hash of 1234"), @@ -102,12 +104,13 @@ func Test_BasicLoginHandler_LoginFailed(t *testing.T) { userService. On("GetUser", loginJSONStruct). Return(user, nil) + sessionService := mocksSessionService.NewSessionService(t) sessionService. - On("LogUserIn", user, response). - Return(httperrors.AnError) + On("LogUserIn", user). + Return(nil, httperrors.ErrForTests) - controller := controllers.NewBasicAuthentificationController( + controller := controllers.NewBasicAuthenticationController( logger, userService, sessionService, @@ -116,7 +119,6 @@ func Test_BasicLoginHandler_LoginFailed(t *testing.T) { payload, err := controller.BasicLoginHandler(response, request) assert.Error(t, err) assert.Nil(t, payload) - } func Test_BasicLoginHandler_LoginSuccess(t *testing.T) { @@ -128,8 +130,8 @@ func Test_BasicLoginHandler_LoginSuccess(t *testing.T) { } response := httptest.NewRecorder() request := httptest.NewRequest( - "POST", - "/v1/auth/basic/login", + http.MethodPost, + "/login", strings.NewReader(`{ "email": "bob@email.com", "password":"1234" @@ -137,8 +139,8 @@ func Test_BasicLoginHandler_LoginSuccess(t *testing.T) { ) userService := mocksUserService.NewUserService(t) user := &models.User{ - BaseModel: models.BaseModel{ - ID: uuid.Nil, + UUIDModel: model.UUIDModel{ + ID: model.NilUUID, }, Username: "bob", Email: "bob@email.com", @@ -147,12 +149,13 @@ func Test_BasicLoginHandler_LoginSuccess(t *testing.T) { userService. On("GetUser", loginJSONStruct). Return(user, nil) + sessionService := mocksSessionService.NewSessionService(t) sessionService. - On("LogUserIn", user, response). - Return(nil) + On("LogUserIn", user). + Return(models.NewSession(user.ID, time.Duration(5)), nil) - controller := controllers.NewBasicAuthentificationController( + controller := controllers.NewBasicAuthenticationController( logger, userService, sessionService, @@ -160,7 +163,7 @@ func Test_BasicLoginHandler_LoginSuccess(t *testing.T) { payload, err := controller.BasicLoginHandler(response, request) assert.NoError(t, err) - assert.Equal(t, payload, dto.DTOLoginSuccess{ + assert.Equal(t, payload, dto.LoginSuccess{ Email: "bob@email.com", ID: user.ID.String(), Username: user.Username, diff --git a/controllers/info.go b/controllers/info.go index f39f30a2..1c4f95f6 100644 --- a/controllers/info.go +++ b/controllers/info.go @@ -3,34 +3,41 @@ package controllers import ( "net/http" + "github.com/Masterminds/semver/v3" + "github.com/ditrit/badaas/httperrors" - "github.com/ditrit/badaas/persistence/models/dto" - "github.com/ditrit/badaas/resources" ) // The information controller type InformationController interface { - // Return the badaas server informations + // Return the badaas server information Info(response http.ResponseWriter, r *http.Request) (any, httperrors.HTTPError) } // check interface compliance var _ InformationController = (*infoControllerImpl)(nil) -// The InformationController constructor -func NewInfoController() InformationController { - return &infoControllerImpl{} -} - // The concrete implementation of the InformationController -type infoControllerImpl struct{} +type infoControllerImpl struct { + Version *semver.Version +} -// Return the badaas server informations -func (*infoControllerImpl) Info(response http.ResponseWriter, r *http.Request) (any, httperrors.HTTPError) { +// The InformationController constructor +func NewInfoController(version *semver.Version) InformationController { + return &infoControllerImpl{ + Version: version, + } +} - infos := &dto.DTOBadaasServerInfo{ +// Return the badaas server information +func (c *infoControllerImpl) Info(_ http.ResponseWriter, _ *http.Request) (any, httperrors.HTTPError) { + return &BadaasServerInfo{ Status: "OK", - Version: resources.Version, - } - return infos, nil + Version: c.Version.String(), + }, nil +} + +type BadaasServerInfo struct { + Status string `json:"status"` + Version string `json:"version"` } diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index e616ca88..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: '3.5' - -services: - db: - image: cockroachdb/cockroach:latest - ports: - - "26257:26257" - - "8080:8080" # Web based dashboard - command: start-single-node --insecure - volumes: - - "${PWD}/_temp/cockroach-data/crdb:/cockroach/cockroach-data" - - - api: - build: ditrit/badaas:latest # local image - ports: - - "8000:8000" - depends_on: - - db diff --git a/docker/cockroachdb/docker-compose.yml b/docker/cockroachdb/docker-compose.yml new file mode 100644 index 00000000..7b743183 --- /dev/null +++ b/docker/cockroachdb/docker-compose.yml @@ -0,0 +1,22 @@ +# DEVELOPMENT ONLY, DO NOT USE FOR PRODUCTION +version: '3.5' + +services: + db: + container_name: "badaas-test-db" + image: cockroachdb/cockroach:latest + volumes: + - .:/cockroach/files + working_dir: /cockroach + entrypoint: /cockroach/cockroach.sh start-single-node --insecure --log-config-file=files/logs.yaml + ports: + - "5000:26257" + - "8080:8080" # Web based dashboard + environment: + - COCKROACH_USER=badaas + - COCKROACH_DATABASE=badaas_db + healthcheck: + test: curl --fail http://localhost:8080 || exit 1 + interval: 10s + timeout: 5s + retries: 5 diff --git a/scripts/e2e/db/logs.yaml b/docker/cockroachdb/logs.yaml similarity index 96% rename from scripts/e2e/db/logs.yaml rename to docker/cockroachdb/logs.yaml index 4cdfbb24..87058924 100644 --- a/scripts/e2e/db/logs.yaml +++ b/docker/cockroachdb/logs.yaml @@ -48,6 +48,8 @@ sinks: auditable: true sql-exec: channels: [SQL_EXEC] + sql-schema: + channels: [SQL_SCHEMA] sql-slow: channels: [SQL_PERF] sql-slow-internal-only: diff --git a/docker/mysql/docker-compose.yml b/docker/mysql/docker-compose.yml new file mode 100644 index 00000000..bcc350ad --- /dev/null +++ b/docker/mysql/docker-compose.yml @@ -0,0 +1,19 @@ +# DEVELOPMENT ONLY, DO NOT USE FOR PRODUCTION +version: '3.5' + +services: + db: + container_name: "badaas-test-db" + image: mysql:latest + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + MYSQL_DATABASE: 'badaas_db' + MYSQL_USER: 'badaas' + MYSQL_PASSWORD: 'badaas_password2023' + ports: + - '5000:3306' + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 \ No newline at end of file diff --git a/docker/postgresql/docker-compose.yml b/docker/postgresql/docker-compose.yml new file mode 100644 index 00000000..f21a2b2a --- /dev/null +++ b/docker/postgresql/docker-compose.yml @@ -0,0 +1,21 @@ +# DEVELOPMENT ONLY, DO NOT USE FOR PRODUCTION +version: '3.5' + +services: + db: + container_name: "badaas-test-db" + image: postgres:latest + environment: + POSTGRES_USER: badaas + POSTGRES_PASSWORD: badaas_password2023 + POSTGRES_DB: badaas_db + PGDATA: /data/postgres + ports: + - "5000:5432" + volumes: + - .:/docker-entrypoint-initdb.d/ + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 \ No newline at end of file diff --git a/docker/postgresql/init.sql b/docker/postgresql/init.sql new file mode 100644 index 00000000..682131d3 --- /dev/null +++ b/docker/postgresql/init.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; \ No newline at end of file diff --git a/docker/sqlserver/Dockerfile b/docker/sqlserver/Dockerfile new file mode 100644 index 00000000..f78701db --- /dev/null +++ b/docker/sqlserver/Dockerfile @@ -0,0 +1,20 @@ +# TODO should be mcr.microsoft.com/mssql/server:2022-latest when https://github.com/microsoft/mssql-docker/issues/847 is fixed +FROM mcr.microsoft.com/mssql/server:2022-CU5-ubuntu-20.04 + +USER mssql + +# Create a config directory +RUN mkdir -p /usr/config +WORKDIR /usr/config + +# Bundle config source +COPY entrypoint.sh /usr/config/ +COPY configure-db.sh /usr/config/ +COPY setup.sql /usr/config/ + +ENTRYPOINT ["./entrypoint.sh"] + +# Tail the setup logs to trap the process +CMD ["tail -f /dev/null"] + +HEALTHCHECK --interval=15s CMD /opt/mssql-tools/bin/sqlcmd -U sa -P $MSSQL_SA_PASSWORD -Q "select 1" && grep -q "MSSQL CONFIG COMPLETE" ./config.log \ No newline at end of file diff --git a/docker/sqlserver/configure-db.sh b/docker/sqlserver/configure-db.sh new file mode 100755 index 00000000..fc3c2f66 --- /dev/null +++ b/docker/sqlserver/configure-db.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# wait for MSSQL server to start +export STATUS=1 +i=0 + +while [[ $STATUS -ne 0 ]] && [[ $i -lt 60 ]]; do + i=$i+1 + /opt/mssql-tools/bin/sqlcmd -t 1 -U sa -P $MSSQL_SA_PASSWORD -Q "select 1" >> /dev/null + STATUS=$? + sleep 1 +done + +if [ $STATUS -ne 0 ]; then + echo "---------------------------- Error: MSSQL SERVER took more than 60 seconds to start up. ----------------------------------------------" + exit 1 +fi + +echo "======= MSSQL SERVER STARTED ========" | tee -a ./config.log +# Run the setup script to create the DB and the schema in the DB +/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $MSSQL_SA_PASSWORD -d master -i setup.sql + +echo "======= MSSQL CONFIG COMPLETE =======" | tee -a ./config.log \ No newline at end of file diff --git a/docker/sqlserver/docker-compose.yml b/docker/sqlserver/docker-compose.yml new file mode 100644 index 00000000..192f3ac4 --- /dev/null +++ b/docker/sqlserver/docker-compose.yml @@ -0,0 +1,16 @@ +# DEVELOPMENT ONLY, DO NOT USE FOR PRODUCTION +version: "3.5" + +services: + db: + container_name: "badaas-test-db" + build: . + image: badaas/mssqlserver:latest + ports: + - "5000:1433" + environment: + MSSQL_SA_PASSWORD: "badaas2023!" + ACCEPT_EULA: "Y" + MSSQL_DB: badaas_db + MSSQL_USER: badaas + MSSQL_PASSWORD: badaas_password2023 \ No newline at end of file diff --git a/docker/sqlserver/entrypoint.sh b/docker/sqlserver/entrypoint.sh new file mode 100755 index 00000000..98830030 --- /dev/null +++ b/docker/sqlserver/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Start SQL Server +/opt/mssql/bin/sqlservr & + +# Start the script to create the DB and user +/usr/config/configure-db.sh + +# Call extra command +eval $1 \ No newline at end of file diff --git a/docker/sqlserver/setup.sql b/docker/sqlserver/setup.sql new file mode 100644 index 00000000..8ff68fd1 --- /dev/null +++ b/docker/sqlserver/setup.sql @@ -0,0 +1,10 @@ +CREATE DATABASE $(MSSQL_DB); +GO +USE $(MSSQL_DB); +GO +CREATE LOGIN $(MSSQL_USER) WITH PASSWORD = '$(MSSQL_PASSWORD)'; +GO +CREATE USER $(MSSQL_USER) FOR LOGIN $(MSSQL_USER); +GO +ALTER SERVER ROLE sysadmin ADD MEMBER [$(MSSQL_USER)]; +GO \ No newline at end of file diff --git a/docker/test_api/Dockerfile b/docker/test_api/Dockerfile new file mode 100644 index 00000000..fbeba3a2 --- /dev/null +++ b/docker/test_api/Dockerfile @@ -0,0 +1,10 @@ +# DEVELOPMENT ONLY, DO NOT USE FOR PRODUCTION +FROM golang:1.19-alpine +RUN addgroup -S badaas \ + && adduser -S badaas -G badaas +USER badaas +WORKDIR /badaas +ENV CGO_ENABLED=0 +ENV GOOS=linux +ENV GOARCH=amd64 +ENV GOFLAGS=-buildvcs=false \ No newline at end of file diff --git a/scripts/e2e/api/badaas.yml b/docker/test_api/badaas.yml similarity index 85% rename from scripts/e2e/api/badaas.yml rename to docker/test_api/badaas.yml index 831b12d8..71c3ab7e 100644 --- a/scripts/e2e/api/badaas.yml +++ b/docker/test_api/badaas.yml @@ -3,16 +3,15 @@ server: host: "0.0.0.0" # listening on all interfaces timeout: 15 # in seconds pagination: - page: + page: max: 10 - database: - host: e2e-db-1 + host: badaas-test-db port: 26257 sslmode: disable username: root - password: postres + password: postgres name: badaas_db init: retry: 10 diff --git a/docker/test_api/docker-compose.yml b/docker/test_api/docker-compose.yml new file mode 100644 index 00000000..6796447c --- /dev/null +++ b/docker/test_api/docker-compose.yml @@ -0,0 +1,19 @@ +# DEVELOPMENT ONLY, DO NOT USE FOR PRODUCTION +version: '3.5' + +services: + api: + container_name: "badaas-test-api" + build: + context: ../.. + dockerfile: ./docker/test_api/Dockerfile + image: badaas-test-api + volumes: + - ../..:/badaas:ro + entrypoint: go run /badaas/test_e2e/test_api.go --config_path /badaas/docker/test_api/badaas.yml + ports: + - "8000:8000" + restart: always + depends_on: + db: + condition: service_healthy diff --git a/docker/wait_for_api.sh b/docker/wait_for_api.sh new file mode 100755 index 00000000..9c586dde --- /dev/null +++ b/docker/wait_for_api.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +until $(curl --output /dev/null --silent --fail http://localhost:$1); do + printf '.' + sleep 5 +done \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..6283bf6c --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,29 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build +PYTHON = python3 + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +htmlview: html + @ls + $(PYTHON) -c "import webbrowser; from pathlib import Path; \ + webbrowser.open(Path('_build/html/index.html').resolve().as_uri())" + +watch: htmlview + watchmedo shell-command -p '*.rst' -c 'make html' -R -D + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.rst b/docs/README.rst new file mode 100644 index 00000000..75d490a5 --- /dev/null +++ b/docs/README.rst @@ -0,0 +1,59 @@ +:orphan: + +====================================== +BaDaaS documentation quick start guide +====================================== + +This file provides a quick guide on how to compile the BaDaaS documentation. + + +Setup the environment +--------------------- + +To compile the documentation you need Sphinx Python library. To install it +and all its dependencies run the following command from this dir + +:: + + pip install -r requirements.txt + + +Compile the documentation +------------------------- + +To compile the documentation (to classic HTML output) run the following command +from this dir:: + + make html + +Documentation will be generated (in HTML format) inside the ``_build/html`` dir. + + +View the documentation +---------------------- + +To view the documentation run the following command:: + + make htmlview + +This command will fire up your default browser and open the main page of your +(previously generated) HTML documentation. + + +Start over +---------- + +To clean up all generated documentation files and start from scratch run:: + + make clean + +Keep in mind that this command won't touch any documentation source files. + + +Recreating documentation on the fly +----------------------------------- + +There is a way to recreate the doc automatically when you make changes, you +need to install watchdog (``pip install watchdog``) and then use:: + + make watch diff --git a/docs/badaas-orm/advanced_query.rst b/docs/badaas-orm/advanced_query.rst new file mode 100644 index 00000000..f859bc82 --- /dev/null +++ b/docs/badaas-orm/advanced_query.rst @@ -0,0 +1,163 @@ +============================== +Advanced query +============================== + +Dynamic operators +-------------------------------- + +In :doc:`/badaas-orm/query` we have seen how to use the operators +to make comparisons between the attributes of a model and static values such as a string, +a number, etc. But if we want to make comparisons between two or more attributes of +the same type we need to use the dynamic operators. +These, instead of a dynamic value, receive a FieldIdentifier, that is, +an object that identifies the attribute with which the operation is to be performed. + +These identifiers are also generated during the generation of conditions +as attributes of the condition model +(if models.MyModel.Name exists, then conditions.MyModel.Name is generated). + +For example we query all MyModels that has the same value in its Name attribute that +its related MyOtherModel's Name attribute. + +.. code-block:: go + + type MyOtherModel struct { + model.UUIDModel + + Name string + } + + type MyModel struct { + model.UUIDModel + + Name string + + Related MyOtherModel + RelatedID model.UUID + } + + myModels, err := orm.NewQuery[MyModel]( + gormDB, + conditions.MyModel.Related( + conditions.MyOtherModel.NameIs().Dynamic().Eq(conditions.MyModel.Name), + ), + ).Find() + +**Attention**, when using dynamic operators the verification that the FieldIdentifier +is concerned by the query is performed at run time, returning an error otherwise. +For example: + +.. code-block:: go + + type MyOtherModel struct { + model.UUIDModel + + Name string + } + + type MyModel struct { + model.UUIDModel + + Name string + + Related MyOtherModel + RelatedID model.UUID + } + + myModels, err := orm.NewQuery[MyModel]( + gormDB, + conditions.MyModel.NameIs().Dynamic().Eq(conditions.MyOtherModel.Name), + ).Find() + +will respond orm.ErrFieldModelNotConcerned in err. + +All operators supported by badaas-orm that receive any value are available in their dynamic version +after using the Dynamic() method of the FieldIs object. + +Select join +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In case the attribute to be used by the dynamic operator is present more +than once in the query, it will be necessary to select the join to be used, +to avoid getting the error orm.ErrJoinMustBeSelected. +To do this, you must use the SelectJoin method, as in the following example: + +.. code-block:: go + + type ParentParent struct { + model.UUIDModel + } + + type Parent1 struct { + model.UUIDModel + + ParentParent ParentParent + ParentParentID model.UUID + } + + type Parent2 struct { + model.UUIDModel + + ParentParent ParentParent + ParentParentID model.UUID + } + + type Child struct { + model.UUIDModel + + Parent1 Parent1 + Parent1ID model.UUID + + Parent2 Parent2 + Parent2ID model.UUID + } + + models, err := orm.NewQuery[Child]( + gormDB, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + conditions.Child.NameIs().Dynamic().Eq(conditions.ParentParent.Name).SelectJoin( + 0, // for the value 0 (conditions.ParentParent.Name), + 0, // choose the first (0) join (made by conditions.Child.Parent1()) + ), + ).Find() + +Unsafe operators +-------------------------------- + +In case you want to avoid the type validations performed by the operators, +unsafe operators should be used. +Although their use is not recommended, this can be useful when the database +used allows operations between different types or when attributes of different +types map at the same time in the database (see ). + +If it is neither of these two cases, the use of an unsafe operator will result in +an error in the execution of the query that depends on the database used. + +All operators supported by badaas-orm that receive any value are available +in their unsafe version after using the Unsafe() method of the FieldIs object. + + +Unsafe conditions (raw SQL) +-------------------------------- + +In case you need to use operators that are not supported by badaas-orm +(please create an issue in our repository if you think we have forgotten any), +you can always run raw SQL with unsafe.NewCondition, as in the following example: + +.. code-block:: go + + myModels, err := orm.NewQuery[MyModel]( + gormDB, + unsafe.NewCondition[MyModel]("%s.name = NULL"), + ).Find() + +As you can see in the example, "%s" can be used in the raw SQL to be replaced +by the table name of the model to which the condition belongs. + +Of course, its use is not recommended because it can generate errors in the execution +of the query that will depend on the database used. \ No newline at end of file diff --git a/docs/badaas-orm/concepts.rst b/docs/badaas-orm/concepts.rst new file mode 100644 index 00000000..8e8f6c1c --- /dev/null +++ b/docs/badaas-orm/concepts.rst @@ -0,0 +1,164 @@ +============================== +Concepts +============================== + +Model +------------------------------ + +A model is any object (struct) of go that you want to persist +in the database and on which you can perform queries. +For this, the struct must have an embedded badaas-orm base model. + +For details visit :ref:`badaas-orm/declaring_models:model declaration`. + +Base model +----------------------------- + +It is a struct that when embedded allows your structures to become BaDaaS models, +adding attributes ID, CreatedAt, UpdatedAt and DeletedAt attributes and the possibility to persist, +create conditions and perform queries on these structures. + +For details visit :ref:`badaas-orm/declaring_models:base models`. + +Model ID +----------------------------- + +The id is a unique identifier needed to persist a model in the database. +It can be a model.UIntID or a model.UUID, depending on the base model used. + +For details visit :ref:`badaas-orm/declaring_models:base models`. + +Auto Migration +---------------------------------------------------------- + +To persist the models it is necessary to migrate the database, +so that the structure of the tables corresponds to the definition of the model. +This migration is performed by gorm through the gormDB. + +For details visit :ref:`badaas-orm/connecting_to_a_database:migration`. + +GormDB +----------------------------- + +GormDB is a gorm.DB object that allows communication with the database. +This object allows us to perform CUD (create, update and delete) +operations. While read operations are also possible, +badaas-orm provides us the :ref:`badaas-orm/concepts:compilable query system` +that is more complete and secure that gorm's query system. + +For details visit :ref:`badaas-orm/connecting_to_a_database:connection`. + +Condition +----------------------------- + +Conditions are the basis of the badaas-orm query system, every query is composed of a set of conditions. +Conditions belong to a particular model and there are 4 different types: +WhereConditions, ConnectionConditions, JoinConditions and PreloadConditions. + +For details visit :doc:`/badaas-orm/query`. + +WhereCondition +----------------------------- + +Type of condition that allows filters to be made on the model to which they belong +and an attribute of this model. These filters are performed through operators. + +For details visit :doc:`/badaas-orm/query`. + +ConnectionCondition +----------------------------- + +Type of condition that allows the use of logical operators +(and, or, or, not) between WhereConditions. + +For details visit :doc:`/badaas-orm/query`. + +JoinCondition +----------------------------- + +Condition type that allows to navigate relationships between models, +which will result in a join in the executed query +(don't worry, if you don't know what a join is, +you don't need to understand the queries that badaas-orm executes). + +For details visit :doc:`/badaas-orm/query`. + +PreloadCondition +----------------------------- + +Type of condition that allows retrieving information from a model as a result of the database (preload). +This information can be all its attributes and/or another model that is related to it. + +For details visit :doc:`/badaas-orm/preloading`. + +Operator +----------------------------- + +Concept similar to database operators, +which allow different operations to be performed on an attribute of a model, +such as comparisons, predicates, pattern matching, etc. + +Operators can be classified as static, dynamic and unsafe. + +For details visit :doc:`/badaas-orm/query`. + +Static operator +----------------------------- + +Static operators are those that perform operations on an attribute and static values, +such as a boolean value, an integer, etc. + +For details visit :doc:`/badaas-orm/query`. + +Dynamic operator +----------------------------- + +Dynamic operators are those that perform operations between an attribute and other attributes, +either from the same model or from a different model, as long as the type of these attributes is the same. + +For details visit :doc:`/badaas-orm/advanced_query`. + +Unsafe operator +----------------------------- + +Unsafe operators are those that can perform operations between an attribute and +any type of value or attribute. + +For details visit :doc:`/badaas-orm/advanced_query`. + +Nullable types +----------------------------- + +Nullable types are the types provided by the sql library +that are a nullable version of the basic types: +sql.NullString, sql.NullTime, sql.NullInt64, sql.NullInt32, +sql.NullBool, sql.NullFloat64, etc.. + +For details visit . + +Compilable query system +----------------------------- + +The set of conditions that are received by the read operations of the +`orm.NewQuery` method form the badaas-orm compilable query system. +It is so named because the conditions will verify at compile time that the query to be executed is correct. + +For details visit :ref:`badaas-orm/query:conditions`. + +Conditions generation +---------------------------- + +Conditions are the basis of the compilable query system. +They are generated for each model and attribute and can then be used. +Their generation is done with badaas-cli. + +For details visit :ref:`badaas-orm/query:Conditions generation`. + +Relation getter +----------------------------------- + +Relationships between objects can be loaded from the database using PreloadConditions. +In order to safely navigate the relations in the loaded model badaas-orm provides methods +called "relation getters". + +For details visit :doc:`/badaas-orm/preloading`. \ No newline at end of file diff --git a/docs/badaas-orm/connecting_to_a_database.rst b/docs/badaas-orm/connecting_to_a_database.rst new file mode 100644 index 00000000..590fa95c --- /dev/null +++ b/docs/badaas-orm/connecting_to_a_database.rst @@ -0,0 +1,24 @@ +============================== +Connecting to a database +============================== + +Connection +----------------------------- + +badaas-orm supports the databases MySQL, PostgreSQL, SQLite, SQL Server using gorm's driver. +Some databases may be compatible with the mysql or postgres dialect, +in which case you could just use the dialect for those databases (from which CockroachDB is tested). + +To communicate with the database badaas-orm need a :ref:`GormDB ` object. +To create it, you can use the function `orm.Open` that will allow you to connect to a database +using the specified dialector. This function is equivalent to `gorm.Open` +but with the difference that in case of not adding any configuration, +the badaas-orm default logger will be configured instead of the gorm one. +For details about this logger visit :doc:`/badaas-orm/logger`. +For details about gorm configuration visit `gorm documentation `_. + +Migration +---------------------------- + +Migration is done by gorm using the `gormDB.AutoMigrate` method. +For details visit `gorm docs `_. \ No newline at end of file diff --git a/docs/badaas-orm/declaring_models.rst b/docs/badaas-orm/declaring_models.rst new file mode 100644 index 00000000..94ef757a --- /dev/null +++ b/docs/badaas-orm/declaring_models.rst @@ -0,0 +1,173 @@ +============================== +Declaring models +============================== + +Model declaration +----------------------- + +The badaas-orm :ref:`model ` declaration is based on the GORM model declaration, +so its definition, conventions, tags and associations are compatible with badaas-orm. +For details see `gorm documentation `_. +On the contrary, badaas-orm presents some differences/extras that are explained in this section. + +Base models +----------------------- + +To be considered a model, your structures must have embedded one of the +:ref:`base models ` provided by badaas-orm: + +- `model.UUIDModel`: Model identified by a model.UUID (Random (Version 4) UUID). +- `model.UIntModel`: Model identified by a model.UIntID (auto-incremental uint). + +Both base models provide date created, updated and `deleted `_. + +To use them, simply embed the desired model in any of your structs: + +.. code-block:: go + + type MyModel struct { + model.UUIDModel + + Name string + Email *string + Age uint8 + Birthday *time.Time + MemberNumber sql.NullString + ActivatedAt sql.NullTime + // ... + } + +Type of attributes +----------------------- + +As we can see in the example in the previous section, +the attributes of your models can be of multiple types, +such as basic go types, pointers, and :ref:`nullable types `. + +This difference can generate differences in the information that is stored in the database, +since saving a model created as follows: + +.. code-block:: go + + MyModel{} + +will save a empty string for Name but a null for the Email and the MemberNumber. + +The use of nullable types is strongly recommended and badaas-orm takes into account +their use in each of its functionalities. + +Associations +----------------------- + +All associations provided by GORM are supported. +For more information see , +, and +. +However, in this section we will give some differences in badaas-orm and +details that are not clear in this documentation. + +IDs +^^^^^^^^^^^^^^^^^^^^^ + +Since badaas-orm base models use model.UUID or model.UIntID to identify the models, +the type of id used in a reference to another model is the corresponding one of these two, +for example: + +.. code-block:: go + + type ModelWithUUID struct { + model.UUIDModel + } + + type ModelWithUIntID struct { + model.UIntModel + } + + type ModelWithReferences struct { + model.UUIDModel + + ModelWithUUID *ModelWithUUID + ModelWithUUIDID *model.UUID + + ModelWithUIntID *ModelWithUIntID + ModelWithUIntIDID *model.UIntID + } + +References +^^^^^^^^^^^^^^^^^^^^^ + +References to other models can be made with or without pointers: + +.. code-block:: go + + type ReferencedModel struct { + model.UUIDModel + } + + type ModelWithPointer struct { + model.UUIDModel + + // reference with pointer + PointerReference *ReferencedModel + PointerReferenceID *model.UUID + } + + type ModelWithoutPointer struct { + model.UUIDModel + + // reference without pointer + Reference ReferencedModel + ReferenceID model.UUID + } + +As in the case of attributes, +this can make a difference when persisting, since one created as follows: + +.. code-block:: go + + ModelWithoutPointer{} + +will also create and save an empty ReferencedModel{}, what may be undesired behavior. +For this reason, although both options are still compatible with badaas-orm, +we recommend the use of pointers for references. +In case the relation is not nullable, use the `not null` tag in the id of the reference, for example: + +.. code-block:: go + + type ReferencedModel struct { + model.UUIDModel + } + + type ModelWithPointer struct { + model.UUIDModel + + // reference with pointer not null + PointerReference *ReferencedModel + PointerReferenceID *model.UUID `gorm:"not null"` + } + +Reverse reference +------------------------------------ + +Although no example within the `gorm's documentation `_ shows it, +when defining relations, we can also put a reference in the reverse direction +to add navigability to our model. +In addition, adding this reverse reference will allow the corresponding conditions +to be generated during condition generation. + +For example: + +.. code-block:: go + + type Related struct { + model.UUIDModel + + YourModel *YourModel + } + + type YourModel struct { + model.UUIDModel + + Related *Related + RelatedID *model.UUID + } \ No newline at end of file diff --git a/docs/badaas-orm/index.rst b/docs/badaas-orm/index.rst new file mode 100644 index 00000000..1cf9607e --- /dev/null +++ b/docs/badaas-orm/index.rst @@ -0,0 +1,36 @@ +============================== +Introduction +============================== + +Badaas-orm is the BaDaaS' component that allows for easy and safe persistence and querying of objects but +it can be used both within a BaDaaS application and independently. + +It's built on top of `gorm `_, +a library that actually provides the functionality of an ORM: mapping objects to tables in the SQL database. +While gorm does this job well with its automatic migration +then performing queries on these objects is somewhat limited, +forcing us to write SQL queries directly when they are complex. +Badaas-orm seeks to address these limitations with a query system that: + +- Is compile-time safe: + its query system is validated at compile time to avoid errors such as + comparing attributes that are of different types, + trying to use attributes or navigate relationships that do not exist, + using information from tables that are not included in the query, etc. +- Is easy to use: + the use of this system does not require knowledge of databases, + SQL languages or complex concepts. + Writing queries only requires programming in go and the result is easy to read. +- Is designed for real applications: + the query system is designed to work well in real-world cases where queries are complex, + require navigating multiple relationships, performing multiple comparisons, etc. +- Is designed so that developers can focus on the business model: + its queries allow easy retrieval of model relationships to apply business logic to the model + and it provides mechanisms to avoid errors in the business logic due to mistakes in loading + information from the database. +- It is designed for high performance: + the query system avoids as much as possible the use of reflection and aims + that all the necessary model data can be retrieved in a single query to the database. + +To see how to start using badaas-orm in your project you can read the :doc:`quickstart`. +If you are interested in learning how to use the features provided by badaas-orm you can read the :doc:`tutorial`. diff --git a/docs/badaas-orm/logger.rst b/docs/badaas-orm/logger.rst new file mode 100644 index 00000000..751aa7b5 --- /dev/null +++ b/docs/badaas-orm/logger.rst @@ -0,0 +1,122 @@ +============================== +Logger +============================== + +When connecting to the database, i.e. when creating the `gorm.DB` object, +it is possible to configure the type of logger to use, the logging level, among others. +As explained in the :ref:`connection section `, +this can be done by using the `orm.Open` method: + +.. code-block:: go + + gormDB, err = orm.Open( + dialector, + &gorm.Config{ + Logger: logger.Default, + }, + ) + +Any logger that complies with `logger.Interface` can be configured. + +Log levels +------------------------------ + +The log levels provided by badaas-orm are the same as those of gorm: + +- `logger.Error`: To only view error messages in case they occur during the execution of a sql query. +- `logger.Warn`: The previous level plus warnings for execution of queries and transactions that take + longer than a certain time + (configurable with SlowQueryThreshold and SlowTransactionThreshold respectively, 200ms by default). +- `logger.Info`: The previous level plus information messages for each query and transaction executed. + +Transactions +------------------ + +For the logs corresponding to transactions +(slow transactions and transaction execution) +to be performed, it is necessary to use the orm.Transaction method. + +Default logger +------------------------------- + +badaas-orm provides a default logger that will print Slow SQL and happening errors. + +You can create one with the default configuration using +(take into account that logger is github.com/ditrit/badaas/orm/logger +and gormLogger is gorm.io/gorm/logger): + +.. code-block:: go + + logger.Default + +or use `logger.New` to customize it: + +.. code-block:: go + + logger.New(gormLogger.Config { + SlowQueryThreshold: 200 * time.Millisecond, + SlowTransactionThreshold: 200 * time.Millisecond, + LogLevel: gormLogger.Warn, + IgnoreRecordNotFoundError: false, + ParameterizedQueries: false, + Colorful: true, + }) + +The LogLevel is also configurable via the `ToLogMode` method. + +**Example** + +.. code-block:: bash + + standalone/example.go:30 [10.392ms] [rows:1] INSERT INTO "products" ("id","created_at","updated_at","deleted_at","string","int","float","bool") VALUES ('4e6d837b-5641-45c9-a028-e5251e1a18b1','2023-07-21 17:19:59.563','2023-07-21 17:19:59.563',NULL,'',1,0.000000,false) + +Zap logger +------------------------------ + +badaas-orm provides the possibility to use `zap `_ as logger. +For this, there is a package called `gormzap`. +The information displayed by the zap logger will be the same as if we were using the default logger +but in a structured form, with the following information: + +* level: ERROR, WARN or DEBUG +* message: + + * query_error for errors during the execution of a query (ERROR) + * query_slow for slow queries (WARN) + * transaction_slow for slow transactions (WARN) + * query_exec for query execution (DEBUG) + * transaction_exec for transaction execution (DEBUG) +* error: (for errors only) +* elapsed_time: query or transaction execution time +* rows_affected: number of rows affected by the query +* sql: query executed + +You can create one with the default configuration using: + +.. code-block:: go + + gormzap.NewDefault(zapLogger) + +where `zapLogger` is a zap logger, or use `gormzap.New` to customize it: + +.. code-block:: go + + gormzap.New(zapLogger, logger.Config { + LogLevel: logger.Warn, + SlowQueryThreshold: 200 * time.Millisecond, + SlowTransactionThreshold: 200 * time.Millisecond, + IgnoreRecordNotFoundError: false, + ParameterizedQueries: false, + }) + +The LogLevel is also configurable via the `ToLogMode` method. +Any configuration of the zap logger is done directly during its creation following the +`zap documentation `_. +Note that the zap logger has its own level setting, so the lower of the two settings +will be the one finally used. + +**Example** + +.. code-block:: bash + + DEBUG fx/example.go:107 query_exec {"elapsed_time": "3.291981ms", "rows_affected": "1", "sql": "SELECT products.* FROM \"products\" WHERE products.int = 1 AND \"products\".\"deleted_at\" IS NULL"} diff --git a/docs/badaas-orm/preloading.rst b/docs/badaas-orm/preloading.rst new file mode 100644 index 00000000..581060c6 --- /dev/null +++ b/docs/badaas-orm/preloading.rst @@ -0,0 +1,138 @@ +============================== +Preloading +============================== + +PreloadConditions +--------------------------- + +During the :ref:`conditions generation ` the following +methods will also be created for the condition models: + +- Preload() will allow to preload this model when doing a query. +- Preload() for each of the relations of your model, + where is the name of the attribute that creates the relation, + to preload that the related object when doing a query. + This is really just a facility that translates to using the JoinCondition of + that relation and then the Preload method of the related model. +- PreloadRelation() to preload all the related models of your model + (only generated if the model has at least one relation). + +Examples +---------------------------------- + +**Preload a related model** + +In this example we query all MyModels and preload whose related MyOtherModel. + +.. code-block:: go + + type MyOtherModel struct { + model.UUIDModel + } + + type MyModel struct { + model.UUIDModel + + Related MyOtherModel + RelatedID model.UUID + } + + myModels, err := orm.NewQuery[MyModel]( + gormDB, + conditions.MyModel.Related( + conditions.Related.Preload(), + ), + ).Find() + +Or using the PreloadRelation method to avoid the JoinCondition +(only useful when you don't want to add other conditions to that Join): + +.. code-block:: go + + myModels, err := orm.NewQuery[MyModel]( + gormDB, + conditions.MyModel.PreloadRelated(), + ).Find() + +**Nested preloads** + +.. code-block:: go + + type Parent struct { + model.UUIDModel + } + + type MyOtherModel struct { + model.UUIDModel + + Parent Parent + ParentID model.UUID + } + + type MyModel struct { + model.UUIDModel + + Related MyOtherModel + RelatedID model.UUID + } + + myModels, err := orm.NewQuery[MyModel]( + gormDB, + conditions.MyModel.Related( + conditions.MyOtherModel.PreloadParent(), + ), + ).Find() + +As we can see, it is not necessary to add the preload to all joins, +it is enough to do it in the deepest one, +to recover, in this example, both Related and Parent. + +Relation getters +-------------------------------------- + +At the moment, with the PreloadConditions, we can choose whether or not to preload a relation. +The problem is that once we get the result of the query, we cannot determine if a null value +corresponds to the fact that the relation is really null or that the preload was not performed, +which means a big risk of making decisions in our business logic on incomplete information. + +For this reason, badaas-orm provides the Relation getters. +These are methods that will be added to your models to safely navigate a relation, +responding `errors.ErrRelationNotLoaded` in case you try to navigate a relation +that was not loaded from the database. +They are created in a file called badaas-orm.go in your model package when +:ref:`generating conditions `. + +Here is an example of its use: + +.. code-block:: go + + type MyOtherModel struct { + model.UUIDModel + } + + type MyModel struct { + model.UUIDModel + + Related MyOtherModel + RelatedID model.UUID + } + + myModel, err := orm.NewQuery[MyModel]( + conditions.MyModel.PreloadRelated(), + ).FindOne() + + if err == nil { + firstRelated, err := myModel.GetRelated() + if err == nil { + // you can safely apply your business logic + } else { + // err is errors.ErrRelationNotLoaded + } + } + +Unfortunately, these relation getters cannot be created in all cases but only in those in which: + +- The relation is made with an object directly instead of a pointer + (which is not recommended as described :ref:`here `). +- The relation is made with pointers and the foreign key (typically the ID) is in the same model. +- The relation is made with a pointer to a list. \ No newline at end of file diff --git a/docs/badaas-orm/query.rst b/docs/badaas-orm/query.rst new file mode 100644 index 00000000..96d299d9 --- /dev/null +++ b/docs/badaas-orm/query.rst @@ -0,0 +1,228 @@ +============================== +Query +============================== + +Create, Save and Delete methods are done directly with gormDB object using the corresponding methods. +For details visit +, and . +On the other hand, read (query) operations are provided by badaas-orm via its compilable query system. + +Query creation +----------------------- + +To create a query you must use the orm.NewQuery[models.MyModel] method, +where models.MyModel is the model you expect this query to answer. +This function takes as parameters the :ref:`transaction ` +on which to execute the query and the query :ref:`badaas-orm/query:conditions`. + +Transactions +-------------------- + +To execute transactions badaas-orm provides the function orm.Transaction. +The function passed by parameter will be executed inside a gorm transaction +(for more information visit https://gorm.io/docs/transactions.html). +Using this method will also allow the transaction execution time to be logged. + +Query methods +------------------------ + +The `orm.Query` object obtained using `orm.NewQuery` has different methods that +will allow you to obtain the results of the query: + +Modifier methods +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Modifier methods are those that modify the query in a certain way, affecting the results obtained: +- Limit: specify the number of models to be retrieved. +- Offset: specify the number of models to skip before starting to return the results. +- Ascending: specify an ascending order when retrieving models. +- Descending: specify a descending order when retrieving models from database. + +Finishing methods +^^^^^^^^^^^^^^^^^^^^^^^ + +Finishing methods are those that cause the query to be executed and the result(s) of the query to be returned: + +- First: finds the first model ordered by primary key. +- Take: finds the first model returned by the database in no specified order. +- Last: finds the last model ordered by primary key. +- FindOne: finds the only one model that matches given conditions or returns error if 0 or more than 1 are found. +- Find: finds list of models that meet the conditions. + +Conditions +------------------------ + +The set of conditions that are received by the `orm.NewQuery` method +form the badaas-orm compilable query system. +It is so named because the conditions will verify at compile time that the query to be executed is correct. + +These conditions are objects of type Condition that contain the +necessary information to perform the queries in a safe way. +They are generated from the definition of your models using badaas-cli. + +Conditions generation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The generation of conditions is done with badaas-cli. For this, we need to install badaas-cli: + +.. code-block:: bash + + go install github.com/ditrit/badaas-cli + +Then, inside our project we will have to create a package called conditions +(or another name if you wish) and inside it a file with the following content: + +.. code-block:: go + + package conditions + + //go:generate badaas-cli gen conditions ../models_path_1 ../models_path_2 + +where ../models_path_1 ../models_path_2 are the relative paths between the package conditions +and the packages containing the definition of your models (can be only one). + +Now, from the root of your project you can execute: + +.. code-block:: bash + + go generate ./... + +and the conditions for each of your models will be created in the conditions package. + +Use of the conditions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +After performing the conditions generation, +your conditions package will have a replica of your models package, +i.e. if, for example, the type models.MyModel is part of your models, +the variable conditions.MyModel will be in the conditions package. +This variable is called the condition model and it has: + +- An attribute for each attribute of your original model with the same name + (if models.MyModel.Name exists, then conditions.MyModel.Name is generated), + of type FieldIdentifier that allows to use that attribute in queries + (for :ref:`dynamic conditions ` for example). +- A method for each attribute of your original model with the same name + Is + (if models.MyModel.Name exists, then conditions.MyModel.NameIs() is generated), + which will allow you to create operations for that attribute in your queries. +- A method for each relation of your original model with the same name + (if models.MyModel.MyOtherModel exists, then conditions.MyModel.MyOtherModel() is generated), + which will allow you to perform joins in your queries. +- Methods for :doc:`/badaas-orm/preloading`. + +Then, combining these conditions, the Connection Conditions (orm.And, orm.Or, orm.Not) +you will be able to make all the queries you need in a safe way. + +Examples +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Filter by an attribute** + +In this example we query all MyModel that has "a_string" in the Name attribute. + +.. code-block:: go + + type MyModel struct { + model.UUIDModel + + Name string + } + + myModels, err := orm.NewQuery[MyModel]( + gormDB, + conditions.MyModel.NameIs().Eq("a_string"), + ).Find() + +**Filter by an attribute of a related model** + +In this example we query all MyModels whose related MyOtherModel has "a_string" in its Name attribute. + +.. code-block:: go + + type MyOtherModel struct { + model.UUIDModel + + Name string + } + + type MyModel struct { + model.UUIDModel + + Related MyOtherModel + RelatedID model.UUID + } + + myModels, err := orm.NewQuery[MyModel]( + gormDB, + conditions.MyModel.Related( + conditions.MyOtherModel.NameIs().Eq("a_string"), + ), + ).Find() + +**Multiple conditions** + +In this example we query all MyModels that has a 4 in the Code attribute and +whose related MyOtherModel has "a_string" in its Name attribute. + +.. code-block:: go + + type MyOtherModel struct { + model.UUIDModel + + Name string + } + + type MyModel struct { + model.UUIDModel + + Code int + + Related MyOtherModel + RelatedID model.UUID + } + + myModels, err := orm.NewQuery[MyModel]( + gormDB, + conditions.MyModel.CodeIs().Eq(4), + conditions.MyModel.Related( + conditions.MyOtherModel.NameIs().Eq("a_string"), + ), + ).Find() + +Operators +------------------------ + +The different operators to use inside your queries are defined by +the methods of the FieldIs type, which is returned when using, for example, +the conditions.MyModel.CodeIs() method. +Below you will find the complete list of available operators: + +- Eq(value): EqualTo +- NotEq(value): NotEqualTo +- Lt(value): LessThan +- LtOrEq(value): LessThanOrEqualTo +- Gt(value): GreaterThan +- GtOrEq(value): GreaterThanOrEqualTo +- Null() +- NotNull() +- Between(v1, v2): Equivalent to v1 < attribute < v2 +- NotBetween(v1, v2): Equivalent to NOT (v1 < attribute < v2) +- True() (Not supported by: sqlserver) +- NotTrue() (Not supported by: sqlserver) +- False() (Not supported by: sqlserver) +- NotFalse() (Not supported by: sqlserver) +- Unknown() (Not supported by: sqlserver, sqlite) +- NotUnknown() (Not supported by: sqlserver, sqlite) +- Distinct(value) (Not supported by: mysql) +- NotDistinct(value) (Not supported by: mysql) +- Like(pattern) +- In(values) +- NotIn(values) + +In addition to these, badaas-orm gives the possibility to use operators +that are only supported by a certain database (outside the standard). +These operators can be found in , +, + +and . +To use them, use the Custom method of FieldIs type. \ No newline at end of file diff --git a/docs/badaas-orm/quickstart.rst b/docs/badaas-orm/quickstart.rst new file mode 100644 index 00000000..d00011dc --- /dev/null +++ b/docs/badaas-orm/quickstart.rst @@ -0,0 +1,82 @@ +============================== +Quickstart +============================== + +To integrate badaas-orm into your project, you can head to the +`quickstart `_. + +Refer to its README.md for running it. + +Understand it +---------------------------------- + +Once you have started your project with `go init`, you must add the dependency to BaDaaS: + +.. code-block:: bash + + go get -u github.com/ditrit/badaas + +Create a package for your :ref:`models `, for example: + +.. code-block:: go + + package models + + import ( + "github.com/ditrit/badaas/orm/model" + ) + + type MyModel struct { + model.UUIDModel + + Name string + } + +Once done, you can :ref:`generate the conditions ` +to perform queries on them. +Create a new package named conditions and add a file with the following content: + +.. code-block:: go + + package conditions + + //go:generate badaas-cli gen conditions ../models + +Then, you can generate the conditions using `badaas-cli` as described in the README.md. + +In main.go create a main function that creates a :ref:`gorm.DB ` +that allows connection with the database and call the :ref:`AutoMigrate ` +method with the models you want to be persisted: + +.. code-block:: go + + func main() { + gormDB, err := NewDBConnection() + if err != nil { + panic(err) + } + + err = gormDB.AutoMigrate( + models.MyModel{}, + ) + if err != nil { + panic(err) + } + + // You are ready to do queries with orm.NewQuery[models.MyModel] + } + + func NewDBConnection() (*gorm.DB, error) { + return orm.Open( + postgres.Open(orm.CreatePostgreSQLDSN("localhost", "root", "postgres", "disable", "badaas_db", 26257)), + &gorm.Config{ + Logger: logger.Default.ToLogMode(logger.Info), + }, + ) + } + +Use it +---------------------- + +Now that you know how to integrate badaas-orm into your project, +you can learn how to use it by following the :doc:`tutorial`. \ No newline at end of file diff --git a/docs/badaas-orm/tutorial.rst b/docs/badaas-orm/tutorial.rst new file mode 100644 index 00000000..f4e0bbb3 --- /dev/null +++ b/docs/badaas-orm/tutorial.rst @@ -0,0 +1,249 @@ +============================== +Tutorial +============================== + +In this short tutorial you will learn the main functionalities of badaas-orm. +The code to be executed in each step can be found in this `repository `_. + +Model and data +----------------------- + +In the file `models/models.go` you find the definition of the following model: + +.. image:: /img/badaas-orm-tutorial-model.png + :width: 700 + :alt: badaas-orm tutorial model + +For details about the definition of models you can read :doc:`/badaas-orm/declaring_models`. + +In `sqlite:db` you will find a sqlite database with the following data: + +.. list-table:: Countries + :header-rows: 1 + + * - ID + - Name + - CapitalID + * - 3739a825-bc5c-4350-a2bc-6e77e22fe3f4 + - France + - eaa480a3-694e-4be3-9af5-ad935cdd57e2 + * - 0c4404f6-83c2-4bdf-93d5-a5ff2fe4f921 + - United States of America + - df44272e-c3db-4e18-876c-f9f579488716 + +.. list-table:: Cities + :header-rows: 1 + + * - ID + - Name + - Population + - CountryID + * - eaa480a3-694e-4be3-9af5-ad935cdd57e2 + - Paris + - 2161000 + - 3739a825-bc5c-4350-a2bc-6e77e22fe3f4 + * - df44272e-c3db-4e18-876c-f9f579488716 + - Washington D. C. + - 689545 + - 0c4404f6-83c2-4bdf-93d5-a5ff2fe4f921 + * - 8c3dfc38-1fc6-4ec9-a89b-e41018a54b4a + - Paris + - 25171 + - 0c4404f6-83c2-4bdf-93d5-a5ff2fe4f921 + +As you can see, there are two cities called Paris in this database: +the well known Paris, capital of France and site of the iconic Eiffel tower, +and Paris in the United States of America, site of the Eiffel tower with the cowboy hat +(no joke, just search for paris texas eiffel tower in your favorite search engine). + +In this tutorial we will explore the badaas-orm functions that will allow us to differentiate these two Paris. + +Tutorial 1: simple query +------------------------------- + +In this first tutorial we are going to perform a simple query to obtain all the cities called Paris. + +In the tutorial_1.go file you will find that we can perform this query as follows: + +.. code-block:: go + + cities, err := orm.NewQuery[models.City]( + db, + conditions.City.NameIs().Eq("Paris"), + ).Find() + +We can run this tutorial with `make tutorial_1` and we will obtain the following result: + +.. code-block:: bash + + Cities named 'Paris' are: + 1: &{UUIDModel:{ID:eaa480a3-694e-4be3-9af5-ad935cdd57e2 CreatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Paris Population:2161000 Country: CountryID:3739a825-bc5c-4350-a2bc-6e77e22fe3f4} + 2: &{UUIDModel:{ID:8c3dfc38-1fc6-4ec9-a89b-e41018a54b4a CreatedAt:2023-08-11 16:43:27.468149185 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.468149185 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Paris Population:25171 Country: CountryID:0c4404f6-83c2-4bdf-93d5-a5ff2fe4f921} + +As you can see, in this case we will get both cities which we can differentiate by their population and the id of the country. + +In this tutorial we have used the badaas-orm compilable queries system to get these cities, +for more details you can read :ref:`badaas-orm/query:conditions`. + +Tutorial 2: operators +------------------------------- + +Now we are going to try to obtain only the Paris of France and in a first +approximation we could do it using its population: we will only look for the Paris +whose population is greater than one million inhabitants. + +In the tutorial_2.go file you will find that we can perform this query as follows: + +.. code-block:: go + + cities, err := orm.NewQuery[models.City]( + db, + conditions.City.NameIs().Eq("Paris"), + conditions.City.PopulationIs().Gt(1000000), + ).Find() + +We can run this tutorial with `make tutorial_2` and we will obtain the following result: + +.. code-block:: bash + + Cities named 'Paris' with a population bigger than 1.000.000 are: + 1: &{UUIDModel:{ID:eaa480a3-694e-4be3-9af5-ad935cdd57e2 CreatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Paris Population:2161000 Country: CountryID:3739a825-bc5c-4350-a2bc-6e77e22fe3f4} + +As you can see, in this case we only get one city, Paris in France. + +In this tutorial we have used the operator Gt to obtain this city, +for more details you can read :ref:`badaas-orm/query:Operators`. + +Tutorial 3: modifiers +------------------------------- + +Although in the previous tutorial we achieved our goal of differentiating the two Paris, +the way to do it is debatable since the population of Paris, Texas may increase to over 1000000 someday +and then, the result of this query can change. +Therefore, we will search only for the city with the largest population. + +In the tutorial_3.go file you will find that we can perform this query as follows: + +.. code-block:: go + + parisFrance, err := orm.NewQuery[models.City]( + db, + conditions.City.NameIs().Eq("Paris"), + ).Descending( + conditions.City.Population, + ).Limit(1).FindOne() + +We can run this tutorial with `make tutorial_3` and we will obtain the following result: + +.. code-block:: bash + + City named 'Paris' with the largest population is: &{UUIDModel:{ID:eaa480a3-694e-4be3-9af5-ad935cdd57e2 CreatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Paris Population:2161000 Country: CountryID:3739a825-bc5c-4350-a2bc-6e77e22fe3f4} + +As you can see, again we get only the Paris in France. +As you may have noticed, in this case we have used the `FindOne` method instead of `Find`. +This is because in this case we are sure that the result is a single model, +so instead of getting a list we get a single city. + +In this tutorial we have used query modifier methods, +for more details you can read :ref:`badaas-orm/query:Query methods`. + +Tutorial 4: joins +------------------------------- + +Again, the solution of the previous tutorial is debatable because the evolution +of populations could make Paris, Texas have more inhabitants than Paris, France one day. +Therefore, we are now going to improve this query by obtaining the city called +Paris whose country is called France. + +In the tutorial_4.go file you will find that we can perform this query as follows: + +.. code-block:: go + + parisFrance, err := orm.NewQuery[models.City]( + db, + conditions.City.NameIs().Eq("Paris"), + conditions.City.Country( + conditions.Country.NameIs().Eq("France"), + ), + ).FindOne() + +We can run this tutorial with `make tutorial_4` and we will obtain the following result: + +.. code-block:: bash + + Cities named 'Paris' in 'France' are: + 1: &{UUIDModel:{ID:eaa480a3-694e-4be3-9af5-ad935cdd57e2 CreatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Paris Population:2161000 Country: CountryID:3739a825-bc5c-4350-a2bc-6e77e22fe3f4} + +As you can see, again we get only the Paris in France. + +In this tutorial we have used a condition that performs a join, +for more details you can read :ref:`badaas-orm/query:Use of the conditions`. + +Tutorial 5: preloading +------------------------------- + +You may have noticed that in the results of the previous tutorials the Country field of the cities was null (Country:). +This is because, to ensure performance, badaas-orm will retrieve only the attributes of the model +you are querying (City in this case because the method used is orm.NewQuery[models.City]) +but not of its relationships. If we also want to obtain this data, we must perform preloading. + +In the tutorial_5.go file you will find that we can perform this query as follows: + +.. code-block:: go + + cities, err := orm.NewQuery[models.City]( + db, + conditions.City.NameIs().Eq("Paris"), + conditions.City.PreloadCountry(), + ).Find() + +We can run this tutorial with `make tutorial_5` and we will obtain the following result: + +.. code-block:: bash + + Cities named 'Paris' are: + 1: &{UUIDModel:{ID:eaa480a3-694e-4be3-9af5-ad935cdd57e2 CreatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Paris Population:2161000 Country:0xc0001d1600 CountryID:3739a825-bc5c-4350-a2bc-6e77e22fe3f4} + with country: &{UUIDModel:{ID:3739a825-bc5c-4350-a2bc-6e77e22fe3f4 CreatedAt:2023-08-11 16:43:27.445202858 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.457191337 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:France Capital: CapitalID:eaa480a3-694e-4be3-9af5-ad935cdd57e2} + 2: &{UUIDModel:{ID:8c3dfc38-1fc6-4ec9-a89b-e41018a54b4a CreatedAt:2023-08-11 16:43:27.468149185 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.468149185 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Paris Population:25171 Country:0xc0001d1780 CountryID:0c4404f6-83c2-4bdf-93d5-a5ff2fe4f921} + with country: &{UUIDModel:{ID:0c4404f6-83c2-4bdf-93d5-a5ff2fe4f921 CreatedAt:2023-08-11 16:43:27.462357133 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.479800337 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:United States of America Capital: CapitalID:df44272e-c3db-4e18-876c-f9f579488716} + +As you can see, now the country attribute is a valid pointer to a Country object (Country:0xc0001d1600). +Then the Country object information is accessed with the `GetCountry` method. +This method is not defined in the `models/models.go` file but is a :ref:`relation getter ` +that is generated by badaas-cli together with the conditions. +These methods allow us to differentiate null objects from objects not loaded from the database, +since when trying to browse a relation that was not loaded we will get `errors.ErrRelationNotLoaded`. + +In this tutorial we have used preloading and relation getters, +for more details you can read :doc:`/badaas-orm/preloading`. + +Tutorial 6: dynamic operators +------------------------------- + +So far we have performed operations that take as input a static value (equal to "Paris" or greater than 1000000) +but what if now we would like to differentiate these two Paris from each other based on whether they +are the capital of their country. + +In the tutorial_6.go file you will find that we can perform this query as follows: + +.. code-block:: go + + cities, err := orm.NewQuery[models.City]( + db, + conditions.City.NameIs().Eq("Paris"), + conditions.City.Country( + conditions.Country.CapitalIdIs().Dynamic().Eq(conditions.City.ID), + ), + ).Find() + +We can run this tutorial with `make tutorial_6` and we will obtain the following result: + +.. code-block:: bash + + Cities named 'Paris' that are the capital of their country are: + 1: &{UUIDModel:{ID:eaa480a3-694e-4be3-9af5-ad935cdd57e2 CreatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 UpdatedAt:2023-08-11 16:43:27.451393348 +0200 +0200 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Paris Population:2161000 Country: CountryID:3739a825-bc5c-4350-a2bc-6e77e22fe3f4} + +As you can see, again we only get the Paris in France. + +In this tutorial we have used dynamic conditions, +for more details you can read :ref:`badaas-orm/advanced_query:Dynamic operators`. diff --git a/docs/badaas/configuration.rst b/docs/badaas/configuration.rst new file mode 100644 index 00000000..00794416 --- /dev/null +++ b/docs/badaas/configuration.rst @@ -0,0 +1,211 @@ +============================== +Configuration +============================== + +Methods +------------------------------- + +Badaas use `verdeter `_ to manage it's configuration, +so Badaas is POSIX compliant by default. + +Badaas-cli automatically generates a default configuration in `badaas/config/badaas.yml`, +but you are free to modify it if you need to. + +This can be done using environment variables, configuration files or CLI flags. +CLI flags take priority on the environment variables and the environment variables take +priority on the content of the configuration file. + +As an example we will define the `database.port` configuration key using the 3 methods: + +- Using a CLI flag: :code:`--database.port=1222` +- Using an environment variable: :code:`export BADAAS_DATABASE_PORT=1222` (*dots are replaced by underscores*) +- Using a config file (in YAML here) + +.. code-block:: yaml + + # /etc/badaas/badaas.yml + database: + port: 1222 + +The config file can be placed at `/etc/badaas/badaas.yml` or `$HOME/.config/badaas/badaas.yml` +or in the same folder as the badaas binary `./badaas.yml`. + +If needed, the location can be overridden using the config key `config_path`. + +Config file +---------------------------- + +In this section, we will focus our attention on config files but +we won't forget that we can use environment variables and CLI flags to change Badaas' config. + +The config file can be formatted in any syntax that +`viper `_ supports but we will only use YAML syntax in our docs. + +To see a complete example of a working config file: head to +`badaas.example.yml `_. + +Database +^^^^^^^^^^^^^^^^^^^^^^^^ +.. code-block:: yaml + + # The settings for the database. + database: + # The host of the database server. + # (mandatory) + host: e2e-db-1 + + # The port of the database server. + # (mandatory) + port: 26257 + + # The name of the database to use. + name: badaas_db + + # The sslmode of the connection to the database server. + # (mandatory) + sslmode: disable + + # The username of the account on the database server. + # (mandatory) + username: root + + # The password of the account on the database server. + # (mandatory) + password: postgres + + # The settings for the initialization of the database server. + init: + # Number of time badaas will try to establish a connection to the database server. + # default (10) + retry: 10 + + # Waiting time between connection, in seconds. + # default (5) + retryTime: 5 + +Please note that the init section `init:` is not mandatory. +Badaas is suited with a simple but effective retry mechanism that will retry +`database.init.retry` time to establish a connection with the database. +Badaas will wait `database.init.retryTime` seconds between each retry. + +Logger +^^^^^^^^^^^^^^^^^^^^^^^^ + +Badaas use a structured logger that can output json logs in +production and user adapted logs for debug using the `logger.mode` key. + +Badaas offers the possibility to change the log message of the +Middleware Logger but provides a sane default. It is formatted using the Jinja syntax. +The values available are `method`, `url` and `protocol`. + +.. code-block:: yaml + + # The settings for the logger. + logger: + # Either `dev` or `prod` + # default (`prod`) + mode: prod + + # Disable error stacktrace from logs + # default (true) + disableStacktrace: true + + # Threshold for the slow query warning in milliseconds + # default (200) + # use 0 to disable slow query warnings + slowQueryThreshold: 200 + + # Threshold for the slow transaction warning in milliseconds + # default (200) + # use 0 to disable slow transaction warnings + slowTransactionThreshold: 200 + + # If true, ignore gorm.ErrRecordNotFound error for logger + # default (false) + ignoreRecordNotFoundError: false + + # If true, don't include params in the query execution logs + # default (false) + parameterizedQueries: false + + request: + # Change the log emitted when badaas receives a request on a valid endpoint. + template: "Receive {{method}} request on {{url}}" + +HTTP Server +^^^^^^^^^^^^^^^^^^^^^^^^ + +You can change the host Badaas will bind to, the port and the timeout in seconds. + +Additionally you can change the number of elements returned by default for a paginated response + +.. code-block:: yaml + + # The settings for the http server. + server: + # The address to bind badaas to. + # default ("0.0.0.0") + host: "" + + # The port badaas should use. + # default (8000) + port: 8000 + + # The maximum timeout for the http server in seconds. + # default (15) + timeout: 15 + + # The settings for the pagination. + pagination: + page: + # The maximum number of record per page + # default (100) + max: 100 + + +Default values +^^^^^^^^^^^^^^^^^^^^^^^^ + +The section allow to change some settings for the first run. + +.. code-block:: yaml + + # The settings for the first run. + default: + # The admin settings for the first run + admin: + # The admin password for the first run. Won't change is the admin user already exists. + password: admin + +Session management +^^^^^^^^^^^^^^^^^^^^^^^^ + +You can change the way the session service handle user sessions. +Session are extended if the user made a request to badaas in the "roll duration". +The session duration and the refresh interval of the cache can be changed. +They contains some good defaults. + +Please see the diagram below to see what is the roll duration relative to the session duration. +:: + + | session duration | + |<----------------------------------------->| + ----|-------------------------|-----------------|----> time + | | | + |<--------------->| + roll duration + +.. code-block:: yaml + + # The settings for session service + # This section contains some good defaults, don't change those value unless you need to. + session: + # The duration of a user session, in seconds + # Default (14400) equal to 4 hours + duration: 14400 + # The refresh interval in seconds. Badaas refresh it's internal session cache periodically. + # Default (30) + pullInterval: 30 + # The duration in which the user can renew it's session by making a request. + # Default (3600) equal to 1 hour + rollDuration: 3600 \ No newline at end of file diff --git a/docs/badaas/functionalities.rst b/docs/badaas/functionalities.rst new file mode 100644 index 00000000..8026b479 --- /dev/null +++ b/docs/badaas/functionalities.rst @@ -0,0 +1,37 @@ +============================== +Functionalities +============================== + +InfoModule +------------------------------- + +`InfoModule` adds the path `/info`, where the api version will be answered. +To set the version you want to be responded you must provide a function that returns it: + +.. code-block:: go + + func main() { + badaas.BaDaaS.AddModules( + badaas.InfoModule, + ).Provide( + NewAPIVersion, + ).Start() + } + + func NewAPIVersion() *semver.Version { + return semver.MustParse("0.0.0-unreleased") + } + +AuthModule +------------------------------- + +`AuthModule` adds `/login` and `/logout`, which allow us to add authentication to our +application in a simple way: + +.. code-block:: go + + func main() { + badaas.BaDaaS.AddModules( + badaas.AuthModule, + ).Start() + } diff --git a/docs/badaas/quickstart.rst b/docs/badaas/quickstart.rst new file mode 100644 index 00000000..613760d1 --- /dev/null +++ b/docs/badaas/quickstart.rst @@ -0,0 +1,102 @@ +============================== +Quickstart +============================== + +To quickly get badaas up and running, you can head to the +`example `_. +By following its README.md, you will see how to use badaas and it will be util +as a template to start your own project. + +Step-by-step instructions +----------------------------------- + +Once you have started your project with `go init`, you must add the dependency to badaas: + +.. code-block:: bash + + go get -u github.com/ditrit/badaas + +Then, you can use the following structure to configure and start your application + +.. code-block:: go + + func main() { + badaas.BaDaaS.AddModules( + // add badaas modules + ).Provide( + // provide constructors + ).Invoke( + // invoke functions + ).Start() + } + +Config badaas functionalities +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You are free to choose which badaas functionalities you wish to use. +To add them, you must add the corresponding module, for example: + +.. code-block:: go + + func main() { + badaas.BaDaaS.AddModules( + badaas.InfoModule, + badaas.AuthModule, + ).Provide( + NewAPIVersion, + ).Start() + } + + func NewAPIVersion() *semver.Version { + return semver.MustParse("0.0.0-unreleased") + } + +Add your own functionalities +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With the "Provide" and "Invoke" functions you will be able to add your own functionalities to the application. +For example, to add a route you must first provide the constructor of the controller and +then invoke the function that adds this route: + +.. code-block:: go + + func main() { + badaas.BaDaaS.Provide( + NewHelloController, + ).Invoke( + AddExampleRoutes, + ).Start() + } + + type HelloController interface { + SayHello(http.ResponseWriter, *http.Request) (any, httperrors.HTTPError) + } + + type helloControllerImpl struct{} + + func NewHelloController() HelloController { + return &helloControllerImpl{} + } + + func (*helloControllerImpl) SayHello(response http.ResponseWriter, r *http.Request) (any, httperrors.HTTPError) { + return "hello world", nil + } + + func AddExampleRoutes( + router *mux.Router, + jsonController middlewares.JSONController, + helloController HelloController, + ) { + router.HandleFunc( + "/hello", + jsonController.Wrap(helloController.SayHello), + ).Methods("GET") + } + +For details visit :doc:`functionalities`. + +Run it +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Once you have defined the functionalities of your project (the `/hello` route in this case), +you can run the application using the steps described in the example README.md diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..86279d51 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,40 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'BaDaaS' +copyright = '2023, DitRit' +author = 'DitRit' +release = '0.0.1' +version = '0.0.1' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "myst_parser", + 'sphinx.ext.duration', + 'sphinx.ext.doctest', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + "sphinx.ext.autosectionlabel", +] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# Make sure the target is unique +autosectionlabel_prefix_document = True + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] + +# -- Options for EPUB output +epub_show_urls = 'footnote' diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md new file mode 100644 index 00000000..e0ff5f5a --- /dev/null +++ b/docs/contributing/contributing.md @@ -0,0 +1,66 @@ +# Contributing + +Thank you for your interest in BaDaaS! This document provides the guidelines for how to contribute to the project through issues and pull-requests. Contributions can also come in additional ways such as joining the [DitRit discord server](https://discord.gg/zkKfj9gj2C), commenting on issues or pull requests and more. + +## Issues + +### Issue types + +There are 3 types of issues: + +- Bug report: You've found a bug with the code, and want to report it, or create an issue to track the bug. +- Discussion: You have something on your mind, which requires input form others in a discussion, before it eventually manifests as a proposal. +- Feature request: Used for items that propose a new idea or functionality. This allows feedback from others before code is written. + +To ask questions and troubleshoot, please join the [DitRit discord server](https://discord.gg/zkKfj9gj2C) (use the BADAAS channel). + +### Before submitting + +Before you submit an issue, make sure you’ve checked the following: + +1. Check for existing issues + - Before you create a new issue, please do a search in [open issues](https://github.com/ditrit/badaas/issues) to see if the issue or feature request has already been filed. + - If you find your issue already exists, make relevant comments and add your reaction. +2. For bugs + - It’s not an environment issue. + - You have as much data as possible. This usually comes in the form of logs and/or stacktrace. +3. You are assigned to the issue, a branch is created from the issue and the `wip` tag is added if you are also planning to develop the solution. + +## Pull Requests + +All contributions come through pull requests. To submit a proposed change, follow this workflow: + +1. Make sure there's an issue (bug report or feature request) opened, which sets the expectations for the contribution you are about to make +2. Assign yourself to the issue and add the `wip` tag +3. Fork the [repo](https://github.com/ditrit/badaas) and create a new [branch](#branch-naming-policy) from the issue +4. Install the necessary [development environment](developing.md#environment) +5. Create your change and the corresponding [tests](developing.md#tests) +6. Update relevant documentation for the change in `docs/` +7. If changes are necessary in [BaDaaS example](https://github.com/ditrit/badaas-example), [badaas-orm quickstart](https://github.com/ditrit/badaas-orm-quickstart) and [badaas-orm tutorial](https://github.com/ditrit/badaas-orm-tutorial), follow the same workflow there +8. Open a PR (and add links to the example repos' PR if they exist) +9. Wait for the CI process to finish and make sure all checks are green +10. A maintainer of the project will be assigned + +### Use work-in-progress PRs for early feedback + +A good way to communicate before investing too much time is to create a "Work-in-progress" PR and share it with your reviewers. The standard way of doing this is to add a "[WIP]" prefix in your PR’s title and assign the do-not-merge label. This will let people looking at your PR know that it is not well baked yet. + +### Branch naming policy + +`[BRANCH_TYPE]/[BRANCH_NAME]` + +- `BRANCH_TYPE` is a prefix to describe the purpose of the branch. + Accepted prefixes are: + - `feature`, used for feature development + - `bugfix`, used for bug fix + - `improvement`, used for refactor + - `library`, used for updating library + - `prerelease`, used for preparing the branch for the release + - `release`, used for releasing project + - `hotfix`, used for applying a hotfix on main + - `poc`, used for proof of concept +- `BRANCH_NAME` is managed by this regex: `[a-z0-9._-]` (`_` is used as space character). + +## Code of Conduct + +This project has adopted the [Contributor Covenant Code of Conduct](https://github.com/ditrit/badaas/blob/main/CODE_OF_CONDUCT.md) diff --git a/docs/contributing/developing.md b/docs/contributing/developing.md new file mode 100644 index 00000000..b1fe33c9 --- /dev/null +++ b/docs/contributing/developing.md @@ -0,0 +1,96 @@ +# Developing + +This document provides the information you need to know before developing code for a pull request. + +## Environment + +- Install [go](https://go.dev/doc/install) >= v1.20 +- Install project dependencies: `go get` +- Install [docker](https://docs.docker.com/engine/install/) and [compose plugin](https://docs.docker.com/compose/install/) + +## Directory structure + +This is the directory structure we use for the project: + +- `configuration/`: Contains all the configuration holders. Please only use the interfaces, they are all mocked for easy testing. +- `controllers/`: Contains all the http controllers, they handle http requests and consume services. +- `docker/` : Contains the docker, docker-compose and configuration files for different environments. +- `docs/`: Contains the documentation showed for readthedocs.io. +- `httperrors/`: Contains the http errors that can be responded by the http api. Should be moved to `controller/` when services stop using them. +- `logger/`: Contains the logger creation logic. Please don't call it from your own services and code, use the dependency injection system. +- `mocks/`: Contains the mocks generated with `mockery`. +- `orm/` *(Go code)*: Contains the code of the orm used by badaas. +- `persistance/`: + - `database/`: Contains the logic to create a database. + - `models/`: Contains the models. + - `dto/`: Contains the Data Transfer Objects. They are used mainly to decode json payloads. + - `repository/`: Contains the repository interfaces and implementations to manage queries to the database. +- `router/`: Contains http router of badaas and the routes that can be added by the user. + - `middlewares/`: Contains the various http middlewares that we use. +- `services/`: Contains services. + - `auth/protocols/`: Contains the implementations of authentication clients for different protocols. + - `basicauth/`: Handle the authentication using email/password. + - `oidc/`: Handle the authentication via Open-ID Connect. +- `test_e2e/`: Contains all the feature and steps for e2e tests. +- `testintegration/`: Contains all the integration tests. +- `utils/`: Contains functions that can be util all around the project, as managing data structures, time, etc. + +At the root of the project, you will find: + +- The README. +- The changelog. +- The LICENSE file. + +## Tests + +### Dependencies + +Running tests have some dependencies as: `mockery`, `gotestsum`, etc.. Install them with `make install_dependencies`. + +### Linting + +We use `golangci-lint` for linting our code. You can test it with `make lint`. The configuration file is in the default path (`.golangci.yml`). The file `.vscode.settings.json.template` is a template for your `.vscode/settings.json` that formats the code according to our configuration. + +### Unit tests + +We use the standard test suite in combination with [github.com/stretchr/testify](https://github.com/stretchr/testify) to do our unit testing. Mocks are generated using [mockery](https://github.com/vektra/mockery) using the command `make test_generate_mocks`. + +To run them, use: + +```sh +make -k test_unit +``` + +### Integration tests + +Integration tests have a database and the dependency injection system. Badaas-orm is tested on multiple databases. By default, the database used will be postgresql: + +```sh +make test_integration +``` + +To run the tests on another database you can use: `make test_integration_postgresql`, `make test_integration_cockroachdb`, `make test_integration_mysql`, `make test_integration_sqlite`, `make test_integration_sqlserver`. All of them will be verified by our continuous integration system. + +### Feature tests (end to end tests) + +These are black box tests that test BaDaaS using its http api. We use docker to run a Badaas instance in combination with one node of CockroachDB. + +Run: + +```sh +make test_e2e +``` + +The feature files can be found in the `test_e2e/features` folder. + +## Requirements + +To be acceptable, contributions must: + +- Have a good quality of code, based on . +- Have at least 80 percent new code coverage (although a higher percentage may be required depending on the importance of the feature). The tests that contribute to coverage are unit tests and integration tests. +- The features defined in the PR base issue must be explicitly tested by an e2e test or by integration tests in case it is not possible (for badaas-orm features for example). + +## Use of Third-party code + +Third-party code must include licenses. diff --git a/docs/contributing/maintaining.md b/docs/contributing/maintaining.md new file mode 100644 index 00000000..44ce9c41 --- /dev/null +++ b/docs/contributing/maintaining.md @@ -0,0 +1,17 @@ +# Maintaining + +This document is intended for BaDaaS maintainers only. + +## How to release + +Release tag are only done on the `main` branch. We use [Semantic Versioning](https://semver.org/spec/v2.0.0.html) as guideline for the version management. + +Steps to release: + +- Create a new branch labeled `release/vX.Y.Z` from the latest `main`. +- Improve the version number in `changelog.md` and `docs/conf.py`. +- Verify the content of the `changelog.md`. +- Commit the modifications with the label `Release version X.Y.Z`. +- Create a pull request on github for this branch into `main`. +- Once the pull request validated and merged, tag the `main` branch with `vX.Y.Z`. +- After the tag is pushed, make the release on the tag in GitHub. diff --git a/docs/img/badaas-orm-tutorial-model.png b/docs/img/badaas-orm-tutorial-model.png new file mode 100644 index 00000000..3e62059d Binary files /dev/null and b/docs/img/badaas-orm-tutorial-model.png differ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..5d69b754 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,63 @@ +============================== +Introduction +============================== + +Badaas enables the effortless construction of **distributed, resilient, +highly available and secure applications by design**, while ensuring very simple +deployment and management (NoOps). + +.. warning:: + BaDaaS is still under development and each of its components can have a different state of evolution + +Features and components +================================= + +Badaas provides several key features, +each provided by a component that can be used independently and has a different state of evolution: + +- **Authentication** (unstable): Badaas can authenticate users using its internal + authentication scheme or externally by using protocols such as OIDC, SAML, Oauth2... +- **Authorization** (wip_unstable): On resource access, Badaas will check if the user + is authorized using a RBAC model. +- **Distribution** (todo): Badaas is built to run in clusters by default. + Communications between nodes are TLS encrypted using `shoset `_. +- **Persistence** (wip_unstable): Applicative objects are persisted as well as user files. + Those resources are shared across the clusters to increase resiliency. + To achieve this, BaDaaS uses the :doc:`badaas-orm ` component. +- **Querying Resources** (unstable): Resources are accessible via a REST API. +- **Posix compliant** (stable): Badaas strives towards being a good unix citizen and + respecting commonly accepted norms. (see :doc:`badaas/configuration`) +- **Advanced logs management** (todo): Badaas provides an interface to interact with + the logs produced by the clusters. Logs are formatted in json by default. + +Learn how to use BaDaaS following the :doc:`badaas/quickstart`. + +.. toctree:: + :caption: BaDaaS + + self + badaas/quickstart + badaas/functionalities + badaas/configuration + +.. toctree:: + :caption: Badaas-orm + + badaas-orm/index + badaas-orm/quickstart + badaas-orm/tutorial + badaas-orm/concepts + badaas-orm/declaring_models + badaas-orm/connecting_to_a_database + badaas-orm/query + badaas-orm/advanced_query + badaas-orm/preloading + badaas-orm/logger + +.. toctree:: + :caption: Contributing + + contributing/contributing + contributing/developing + contributing/maintaining + Github diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..32bb2452 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.in b/docs/requirements.in new file mode 100644 index 00000000..4cb3091f --- /dev/null +++ b/docs/requirements.in @@ -0,0 +1,3 @@ +Sphinx +sphinx_rtd_theme +myst-parser \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..91598168 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,78 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile docs/requirements.in +# +alabaster==0.7.13 + # via sphinx +babel==2.12.1 + # via sphinx +certifi==2023.7.22 + # via requests +charset-normalizer==3.2.0 + # via requests +docutils==0.18.1 + # via + # myst-parser + # sphinx + # sphinx-rtd-theme +idna==3.4 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.2 + # via + # myst-parser + # sphinx +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # myst-parser +markupsafe==2.1.3 + # via jinja2 +mdit-py-plugins==0.4.0 + # via myst-parser +mdurl==0.1.2 + # via markdown-it-py +myst-parser==2.0.0 + # via -r docs/requirements.in +packaging==23.1 + # via sphinx +pygments==2.16.1 + # via sphinx +pyyaml==6.0.1 + # via myst-parser +requests==2.31.0 + # via sphinx +snowballstemmer==2.2.0 + # via sphinx +sphinx==7.2.4 + # via + # -r docs/requirements.in + # myst-parser + # sphinx-rtd-theme + # sphinxcontrib-applehelp + # sphinxcontrib-devhelp + # sphinxcontrib-htmlhelp + # sphinxcontrib-jquery + # sphinxcontrib-qthelp + # sphinxcontrib-serializinghtml +sphinx-rtd-theme==1.3.0 + # via -r docs/requirements.in +sphinxcontrib-applehelp==1.0.7 + # via sphinx +sphinxcontrib-devhelp==1.0.5 + # via sphinx +sphinxcontrib-htmlhelp==2.0.4 + # via sphinx +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.6 + # via sphinx +sphinxcontrib-serializinghtml==1.1.9 + # via sphinx +urllib3==2.0.4 + # via requests diff --git a/features/api_info.feature b/features/api_info.feature deleted file mode 100644 index f7b6790f..00000000 --- a/features/api_info.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: Test info controller - -Scenario: Server should return ok and current project version - When I request "/info" - Then I expect status code is "200" - And I expect response field "status" is "OK" - And I expect response field "version" is "UNRELEASED" diff --git a/go.mod b/go.mod index 788ae80b..558e615f 100644 --- a/go.mod +++ b/go.mod @@ -3,66 +3,64 @@ module github.com/ditrit/badaas go 1.18 require ( - github.com/Masterminds/squirrel v1.5.3 - github.com/cucumber/godog v0.12.5 + github.com/Masterminds/semver/v3 v3.1.1 github.com/ditrit/verdeter v0.4.0 + github.com/elliotchance/pie/v2 v2.7.0 + github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 + github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 - github.com/jackc/pgconn v1.13.0 - github.com/magiconair/properties v1.8.6 + github.com/magiconair/properties v1.8.7 github.com/noirbizarre/gonja v0.0.0-20200629003239-4d051fd0be61 - github.com/spf13/cobra v1.5.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.13.0 - github.com/stretchr/testify v1.8.1 - go.uber.org/fx v1.18.2 - go.uber.org/zap v1.23.0 - golang.org/x/crypto v0.1.0 - gorm.io/driver/postgres v1.4.5 - gorm.io/gorm v1.24.1 + github.com/spf13/cobra v1.7.0 + github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.4 + go.uber.org/fx v1.19.3 + go.uber.org/zap v1.24.0 + golang.org/x/crypto v0.9.0 + gorm.io/driver/mysql v1.5.1 + gorm.io/driver/postgres v1.5.2 + gorm.io/driver/sqlite v1.5.2 + gorm.io/driver/sqlserver v1.5.1 + gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 + gotest.tools v2.2.0+incompatible ) require ( - github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect - github.com/cucumber/messages-go/v16 v16.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/gofrs/uuid v4.0.0+incompatible // indirect + github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/goph/emperror v0.17.2 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-memdb v1.3.3 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgio v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.1 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.12.0 // indirect - github.com/jackc/pgx/v4 v4.17.2 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect - github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/microsoft/go-mssqldb v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect - github.com/spf13/afero v1.9.2 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/sirupsen/logrus v1.9.2 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/dig v1.15.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - golang.org/x/sys v0.1.0 // indirect - golang.org/x/text v0.4.0 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/dig v1.17.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a7b58dea..d50c8ca7 100644 --- a/go.sum +++ b/go.sum @@ -25,7 +25,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -37,32 +36,23 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= -github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bmuller/arrow v0.0.0-20180318014521-b14bfde8dff2/go.mod h1:+voQMVaya0tr8p3W33Qxj/dKOjZNCepW+k8JJvt91gk= github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 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= @@ -70,64 +60,44 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE= -github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw= -github.com/cucumber/godog v0.12.5 h1:FZIy6VCfMbmGHts9qd6UjBMT9abctws/pQYO/ZcwOVs= -github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6Tm9t5pIc= -github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= -github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY= -github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/ditrit/verdeter v0.3.2-0.20230118160022-0caba70148cd h1:r2NABj0IHkzEtp4ZGaKpWNxsWAhjQU4ezxyELvXObrk= -github.com/ditrit/verdeter v0.3.2-0.20230118160022-0caba70148cd/go.mod h1:sKpWuOvYqNabLN4aNXqeBhcWpt7nf0frwqk0B5M6ax0= github.com/ditrit/verdeter v0.4.0 h1:DzEOFauuXEGNQYP6OgYtHwEyb3w9riem99u0xE/l7+o= github.com/ditrit/verdeter v0.4.0/go.mod h1:sKpWuOvYqNabLN4aNXqeBhcWpt7nf0frwqk0B5M6ax0= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= +github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -163,7 +133,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -187,177 +158,77 @@ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 github.com/goph/emperror v0.17.1/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-memdb v1.3.0/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g= -github.com/hashicorp/go-memdb v1.3.3 h1:oGfEWrFuxtIUF3W2q/Jzt6G85TrMk9ey6XfYLvVe1Wo= -github.com/hashicorp/go-memdb v1.3.3/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 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= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= -github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= -github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= -github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= -github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 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/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/microsoft/go-mssqldb v1.1.0 h1:jsV+tpvcPTbNNKW0o3kiCD69kOHICsfjZ2VcVu2lKYc= +github.com/microsoft/go-mssqldb v1.1.0/go.mod h1:LzkFdl4z2Ck+Hi+ycGOTbL56VEfgoyA2DvYejrNGbRk= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/noirbizarre/gonja v0.0.0-20200629003239-4d051fd0be61 h1:8HaKr2WO2B5XKEFbJE9Z7W8mWC6+dL3jZCw53Dbl0oI= github.com/noirbizarre/gonja v0.0.0-20200629003239-4d051fd0be61/go.mod h1:WboHq+I9Ck8PwKsVFJNrpiRyngXhquRSTWBGwuSWOrg= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -365,66 +236,29 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= -github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -435,67 +269,49 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 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.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE= -go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM= -go.uber.org/fx v1.18.2 h1:bUNI6oShr+OVFQeU8cDNbnN7VFsu+SsjHzUF51V/GAU= -go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= +go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= +go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA= +go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= -go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 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= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -506,6 +322,8 @@ 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-20220321173239-a90fa8a75705 h1:ba9YlqfDGTTQ5aZ2fwOoQ1hf32QySyQkR6ODGDzHlnE= +golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 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= @@ -529,13 +347,11 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -546,7 +362,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -567,6 +382,11 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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= @@ -586,31 +406,24 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -633,13 +446,21 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -648,33 +469,28 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -682,7 +498,6 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -708,8 +523,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 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= @@ -802,36 +617,36 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc= -gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg= -gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.24.1 h1:CgvzRniUdG67hBAzsxDGOAuq4Te1osVMYsa1eQbd4fs= -gorm.io/gorm v1.24.1/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= +gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= +gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= +gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= +gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc= +gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/driver/sqlserver v1.5.1 h1:wpyW/pR26U94uaujltiFGXY7fd2Jw5hC9PB1ZF/Y5s4= +gorm.io/driver/sqlserver v1.5.1/go.mod h1:AYHzzte2msKTmYBYsSIq8ZUsznLJwBdkB2wpI+kt0nM= +gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/go.work b/go.work new file mode 100644 index 00000000..241c23dd --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.18 + +use ( + . + ./test_e2e +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 00000000..8e6223e2 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,338 @@ +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= +github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.20.0/go.mod h1:nR64eD44KQ59Of/ECwt2vUmIK2DKsDzAwTmwmLl8Wpo= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/crypt v0.10.0/go.mod h1:gwTNHQVoOS3xp9Xvz5LLR+1AauC5M6880z5NWzdhOyQ= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.9/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= +go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= +go.etcd.io/etcd/client/v2 v2.305.7/go.mod h1:GQGT5Z3TBuAQGvgPfhR7VPySu/SudxmEkRq9BgzFU6s= +go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +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.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/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.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= diff --git a/http_support_test.go b/http_support_test.go deleted file mode 100644 index b1f5ec7c..00000000 --- a/http_support_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "strconv" - "strings" - - "github.com/cucumber/godog" -) - -const BaseUrl = "http://localhost:8000" - -func (t *TestContext) requestGET(url string) error { - response, err := t.httpClient.Get(fmt.Sprintf("%s%s", BaseUrl, url)) - if err != nil { - return err - } - - t.storeResponseInContext(response) - return nil -} - -func (t *TestContext) storeResponseInContext(response *http.Response) { - t.statusCode = response.StatusCode - - buffer, err := io.ReadAll(response.Body) - if err != nil { - log.Panic(err) - } - response.Body.Close() - json.Unmarshal(buffer, &t.json) -} - -func (t *TestContext) assertStatusCode(_ context.Context, expectedStatusCode int) error { - if t.statusCode != expectedStatusCode { - return fmt.Errorf("expect status code %d but is %d", expectedStatusCode, t.statusCode) - } - return nil -} -func (t *TestContext) assertResponseFieldIsEquals(field string, expectedValue string) error { - value := t.json[field].(string) - if !assertValue(value, expectedValue) { - return fmt.Errorf("expect response field %s is %s but is %s", field, expectedValue, value) - } - return nil -} - -func assertValue(value string, expectedValue string) bool { - return expectedValue == value -} - -func (t *TestContext) requestWithJson(url, method string, jsonTable *godog.Table) error { - payload, err := buildJSONFromTable(jsonTable) - if err != nil { - return err - } - - method, err = checkMethod(method) - if err != nil { - return err - } - request, err := http.NewRequest(method, BaseUrl+url, payload) - if err != nil { - return fmt.Errorf("failed to build request ERROR=%s", err.Error()) - } - response, err := t.httpClient.Do(request) - if err != nil { - return fmt.Errorf("failed to run request ERROR=%s", err.Error()) - } - t.storeResponseInContext(response) - return nil -} - -// build a json payload in the form of a reader from a godog.Table -func buildJSONFromTable(table *godog.Table) (io.Reader, error) { - data := make(map[string]any, 0) - for indexRow, row := range table.Rows { - if indexRow == 0 { - for indexCell, cell := range row.Cells { - if cell.Value != []string{"key", "value", "type"}[indexCell] { - return nil, fmt.Errorf("should have %q as first line of the table", "| key | value | type |") - } - } - } else { - key := row.Cells[0].Value - valueAsString := row.Cells[1].Value - valueType := row.Cells[2].Value - - switch valueType { - case stringValueType: - data[key] = valueAsString - case booleanValueType: - boolean, err := strconv.ParseBool(valueAsString) - if err != nil { - return nil, fmt.Errorf("can't parse %q as boolean for key %q", valueAsString, key) - } - data[key] = boolean - case integerValueType: - integer, err := strconv.ParseInt(valueAsString, 10, 64) - if err != nil { - return nil, fmt.Errorf("can't parse %q as integer for key %q", valueAsString, key) - } - data[key] = integer - case floatValueType: - floatingNumber, err := strconv.ParseFloat(valueAsString, 64) - if err != nil { - return nil, fmt.Errorf("can't parse %q as float for key %q", valueAsString, key) - } - data[key] = floatingNumber - case nullValueType: - data[key] = nil - default: - return nil, fmt.Errorf("type %q does not exists, please use %v", valueType, []string{stringValueType, booleanValueType, integerValueType, floatValueType, nullValueType}) - } - - } - } - bytes, err := json.Marshal(data) - if err != nil { - panic("should not return an error") - } - return strings.NewReader(string(bytes)), nil -} - -const ( - stringValueType = "string" - booleanValueType = "boolean" - integerValueType = "integer" - floatValueType = "float" - nullValueType = "null" -) - -// check if the method is allowed and sanitize the string -func checkMethod(method string) (string, error) { - allowedMethods := []string{http.MethodGet, - http.MethodHead, - http.MethodPost, - http.MethodPut, - http.MethodPatch, - http.MethodDelete, - http.MethodConnect, - http.MethodOptions, - http.MethodTrace} - sanitizedMethod := strings.TrimSpace(strings.ToUpper(method)) - if !contains( - allowedMethods, - sanitizedMethod, - ) { - return "", fmt.Errorf("%q is not a valid HTTP method (please choose between %v)", method, allowedMethods) - } - return sanitizedMethod, nil - -} - -// return true if the set contains the target -func contains[T comparable](set []T, target T) bool { - for _, elem := range set { - if target == elem { - return true - } - } - return false -} diff --git a/httperrors/httperrors.go b/httperrors/httperrors.go index c5a02ecf..8bb408c7 100644 --- a/httperrors/httperrors.go +++ b/httperrors/httperrors.go @@ -5,23 +5,22 @@ import ( "fmt" "net/http" - "github.com/ditrit/badaas/persistence/models/dto" "go.uber.org/zap" -) -var ( - // AnError is an HTTPError instance useful for testing. If the code does not care - // about HTTPError specifics, and only needs to return the HTTPError for example, this - // HTTPError should be used to make the test code more readable. - AnError HTTPError = &HTTPErrorImpl{ - Status: -1, - Err: "TESTING ERROR", - Message: "USE ONLY FOR TESTING", - GolangError: nil, - toLog: true, - } + "github.com/ditrit/badaas/persistence/models/dto" ) +// ErrForTests is an HTTPError instance useful for testing. If the code does not care +// about HTTPError specifics, and only needs to return the HTTPError for example, this +// HTTPError should be used to make the test code more readable. +var ErrForTests HTTPError = &HTTPErrorImpl{ + Status: -1, + Err: "TESTING ERROR", + Message: "USE ONLY FOR TESTING", + GolangError: nil, + toLog: true, +} + type HTTPError interface { error @@ -36,6 +35,8 @@ type HTTPError interface { } // Describe an HTTP error +// +//nolint:errname // this name is correct for a type name type HTTPErrorImpl struct { Status int Err string @@ -46,12 +47,15 @@ type HTTPErrorImpl struct { // Convert an HTTPError to a json string func (httpError *HTTPErrorImpl) ToJSON() string { - dto := &dto.DTOHTTPError{ + dto := &dto.HTTPError{ Error: httpError.Err, Message: httpError.Message, Status: http.StatusText(httpError.Status), } + + //nolint:errchkjson // TODO fix it payload, _ := json.Marshal(dto) + return string(payload) } @@ -70,6 +74,7 @@ func (httpError *HTTPErrorImpl) Write(httpResponse http.ResponseWriter, logger * if httpError.toLog && logger != nil { logHTTPError(httpError, logger) } + http.Error(httpResponse, httpError.ToJSON(), httpError.Status) } @@ -93,18 +98,18 @@ func NewHTTPError(status int, err string, message string, golangError error, toL } } -// A contructor for an HttpError "Not Found" -func NewErrorNotFound(ressourceName string, msg string) HTTPError { +// A constructor for an HttpError "Not Found" +func NewErrorNotFound(resourceName string, msg string) HTTPError { return NewHTTPError( http.StatusNotFound, - fmt.Sprintf("%s not found", ressourceName), + fmt.Sprintf("%s not found", resourceName), msg, nil, false, ) } -// A contructor for an HttpError "Internal Server Error" +// A constructor for an HttpError "Internal Server Error" func NewInternalServerError(errorName string, msg string, err error) HTTPError { return NewHTTPError( http.StatusInternalServerError, @@ -115,7 +120,12 @@ func NewInternalServerError(errorName string, msg string, err error) HTTPError { ) } -// A contructor for an HttpError "Unauthorized Error" +// Constructor for an HttpError "DB Error", a internal server error produced by a query +func NewDBError(err error) HTTPError { + return NewInternalServerError("db error", "database query failed", err) +} + +// A constructor for an HttpError "Unauthorized Error" func NewUnauthorizedError(errorName string, msg string) HTTPError { return NewHTTPError( http.StatusUnauthorized, diff --git a/httperrors/httperrors_test.go b/httperrors/httperrors_test.go index b98b9c77..03ee8390 100644 --- a/httperrors/httperrors_test.go +++ b/httperrors/httperrors_test.go @@ -9,25 +9,27 @@ import ( "net/http/httptest" "testing" - "github.com/ditrit/badaas/httperrors" - "github.com/ditrit/badaas/persistence/models/dto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" + + "github.com/ditrit/badaas/httperrors" + "github.com/ditrit/badaas/persistence/models/dto" ) func TestTojson(t *testing.T) { err := "Error while parsing json" message := "The request body was malformed" - error := httperrors.NewHTTPError(http.StatusBadRequest, err, message, nil, true) - assert.NotEmpty(t, error.ToJSON()) - assert.True(t, json.Valid([]byte(error.ToJSON())), "output json is not valid") + herr := httperrors.NewHTTPError(http.StatusBadRequest, err, message, nil, true) + assert.NotEmpty(t, herr.ToJSON()) + assert.True(t, json.Valid([]byte(herr.ToJSON())), "output json is not valid") - // check if is is correctly deserialized + // check if it is correctly deserialized var content map[string]any - json.Unmarshal([]byte(error.ToJSON()), &content) + + json.Unmarshal([]byte(herr.ToJSON()), &content) _, ok := content["err"] assert.True(t, ok, "\"err\" field should be in the json string") _, ok = content["msg"] @@ -38,29 +40,30 @@ func TestTojson(t *testing.T) { assert.Equal(t, err, content["err"].(string)) assert.Equal(t, message, content["msg"].(string)) assert.Equal(t, http.StatusText(http.StatusBadRequest), content["status"].(string)) - assert.True(t, error.Log()) + assert.True(t, herr.Log()) } func TestLog(t *testing.T) { - error := httperrors.NewHTTPError(http.StatusBadRequest, "err", "message", nil, true) - assert.True(t, error.Log()) - error = httperrors.NewHTTPError(http.StatusBadRequest, "err", "message", nil, false) - assert.False(t, error.Log()) + herr := httperrors.NewHTTPError(http.StatusBadRequest, "err", "message", nil, true) + assert.True(t, herr.Log()) + herr = httperrors.NewHTTPError(http.StatusBadRequest, "err", "message", nil, false) + assert.False(t, herr.Log()) } func TestError(t *testing.T) { - error := httperrors.NewHTTPError(http.StatusBadRequest, "Error while parsing json", "The request body was malformed", nil, true) - assert.Contains(t, error.Error(), error.ToJSON()) + herr := httperrors.NewHTTPError(http.StatusBadRequest, "Error while parsing json", "The request body was malformed", nil, true) + assert.Contains(t, herr.Error(), herr.ToJSON()) } func TestWrite(t *testing.T) { res := httptest.NewRecorder() - error := httperrors.NewHTTPError(http.StatusBadRequest, "Error while parsing json", "The request body was malformed", nil, true) - error.Write(res, zap.L()) + herr := httperrors.NewHTTPError(http.StatusBadRequest, "Error while parsing json", "The request body was malformed", nil, true) + herr.Write(res, zap.L()) bodyBytes, err := io.ReadAll(res.Body) assert.Nil(t, err) assert.NotEmpty(t, bodyBytes) - originalBytes := []byte(error.ToJSON()) + + originalBytes := []byte(herr.ToJSON()) // can't use assert.Contains because it only support strings assert.True(t, bytes.Contains(bodyBytes, originalBytes)) @@ -72,8 +75,8 @@ func TestLogger(t *testing.T) { observedLogger := zap.New(observedZapCore) res := httptest.NewRecorder() - error := httperrors.NewHTTPError(http.StatusBadRequest, "Error while parsing json", "The request body was malformed", nil, true) - error.Write(res, observedLogger) + herr := httperrors.NewHTTPError(http.StatusBadRequest, "Error while parsing json", "The request body was malformed", nil, true) + herr.Write(res, observedLogger) require.Equal(t, 1, observedLogs.Len()) log := observedLogs.All()[0] @@ -88,32 +91,35 @@ func TestLogger(t *testing.T) { func TestNewErrorNotFound(t *testing.T) { ressourceName := "file" - error := httperrors.NewErrorNotFound(ressourceName, "main.css is not accessible") - assert.NotNil(t, error) - assert.False(t, error.Log()) - dto := new(dto.DTOHTTPError) - err := json.Unmarshal([]byte(error.ToJSON()), &dto) + herr := httperrors.NewErrorNotFound(ressourceName, "main.css is not accessible") + assert.NotNil(t, herr) + assert.False(t, herr.Log()) + + dto := new(dto.HTTPError) + err := json.Unmarshal([]byte(herr.ToJSON()), &dto) assert.NoError(t, err) assert.Equal(t, http.StatusText(http.StatusNotFound), dto.Status) assert.Equal(t, fmt.Sprintf("%s not found", ressourceName), dto.Error) } func TestNewInternalServerError(t *testing.T) { - error := httperrors.NewInternalServerError("casbin error", "the ressource is not accessible", nil) - assert.NotNil(t, error) - assert.True(t, error.Log()) - dto := new(dto.DTOHTTPError) - err := json.Unmarshal([]byte(error.ToJSON()), &dto) + herr := httperrors.NewInternalServerError("casbin error", "the ressource is not accessible", nil) + assert.NotNil(t, herr) + assert.True(t, herr.Log()) + + dto := new(dto.HTTPError) + err := json.Unmarshal([]byte(herr.ToJSON()), &dto) assert.NoError(t, err) assert.Equal(t, http.StatusText(http.StatusInternalServerError), dto.Status) } func TestNewUnauthorizedError(t *testing.T) { - error := httperrors.NewUnauthorizedError("json unmarshalling", "nil value whatever") - assert.NotNil(t, error) - assert.True(t, error.Log()) - dto := new(dto.DTOHTTPError) - err := json.Unmarshal([]byte(error.ToJSON()), &dto) + herr := httperrors.NewUnauthorizedError("json unmarshalling", "nil value whatever") + assert.NotNil(t, herr) + assert.True(t, herr.Log()) + + dto := new(dto.HTTPError) + err := json.Unmarshal([]byte(herr.ToJSON()), &dto) assert.NoError(t, err) assert.Equal(t, http.StatusText(http.StatusUnauthorized), dto.Status) } diff --git a/logger/log.go b/logger/log.go index 9170f8e4..894243cc 100644 --- a/logger/log.go +++ b/logger/log.go @@ -3,32 +3,31 @@ package logger import ( "log" - "github.com/ditrit/badaas/configuration" "go.uber.org/zap" -) -const ( - ProductionLogger = "prod" - DevelopmentLogger = "dev" + "github.com/ditrit/badaas/configuration" ) // Return a configured logger func NewLogger(conf configuration.LoggerConfiguration) *zap.Logger { var config zap.Config - if conf.GetMode() == ProductionLogger { + if conf.GetMode() == configuration.ProductionLogger { config = zap.NewProductionConfig() - log.Printf("Log mode use: %s\n", ProductionLogger) + log.Printf("Log mode use: %s\n", configuration.ProductionLogger) } else { config = zap.NewDevelopmentConfig() - log.Printf("Log mode use: %s\n", DevelopmentLogger) - + log.Printf("Log mode use: %s\n", configuration.DevelopmentLogger) } - config.DisableStacktrace = true + + config.DisableStacktrace = conf.GetDisableStacktrace() + logger, err := config.Build() if err != nil { panic(err) } + logger.Info("The logger was successfully initialized") + return logger } diff --git a/logger/log_test.go b/logger/log_test.go index 873a698c..d1431e04 100644 --- a/logger/log_test.go +++ b/logger/log_test.go @@ -3,14 +3,17 @@ package logger import ( "testing" - configurationmocks "github.com/ditrit/badaas/mocks/configuration" "github.com/stretchr/testify/assert" "go.uber.org/zap/zapcore" + + "github.com/ditrit/badaas/configuration" + configurationmocks "github.com/ditrit/badaas/mocks/configuration" ) func TestInitializeDevelopmentLogger(t *testing.T) { conf := configurationmocks.NewLoggerConfiguration(t) - conf.On("GetMode").Return("dev") + conf.On("GetMode").Return(configuration.DevelopmentLogger) + conf.On("GetDisableStacktrace").Return(true) logger := NewLogger(conf) assert.NotNil(t, logger) assert.True(t, logger.Core().Enabled(zapcore.DebugLevel)) @@ -18,7 +21,8 @@ func TestInitializeDevelopmentLogger(t *testing.T) { func TestInitializeProductionLogger(t *testing.T) { conf := configurationmocks.NewLoggerConfiguration(t) - conf.On("GetMode").Return("prod") + conf.On("GetMode").Return(configuration.ProductionLogger) + conf.On("GetDisableStacktrace").Return(true) logger := NewLogger(conf) assert.NotNil(t, logger) assert.False(t, logger.Core().Enabled(zapcore.DebugLevel)) @@ -27,6 +31,7 @@ func TestInitializeProductionLogger(t *testing.T) { func TestInitializeProductionLoggerNoConf(t *testing.T) { conf := configurationmocks.NewLoggerConfiguration(t) conf.On("GetMode").Return("a stupid value") + conf.On("GetDisableStacktrace").Return(true) logger := NewLogger(conf) assert.NotNil(t, logger) assert.True(t, logger.Core().Enabled(zapcore.DebugLevel)) diff --git a/mocks/configuration/CommandInitializer.go b/mocks/configuration/CommandInitializer.go new file mode 100644 index 00000000..14b1686b --- /dev/null +++ b/mocks/configuration/CommandInitializer.go @@ -0,0 +1,42 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + verdeter "github.com/ditrit/verdeter" + mock "github.com/stretchr/testify/mock" +) + +// CommandInitializer is an autogenerated mock type for the CommandInitializer type +type CommandInitializer struct { + mock.Mock +} + +// Init provides a mock function with given fields: command +func (_m *CommandInitializer) Init(command *verdeter.VerdeterCommand) error { + ret := _m.Called(command) + + var r0 error + if rf, ok := ret.Get(0).(func(*verdeter.VerdeterCommand) error); ok { + r0 = rf(command) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewCommandInitializer interface { + mock.TestingT + Cleanup(func()) +} + +// NewCommandInitializer creates a new instance of CommandInitializer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewCommandInitializer(t mockConstructorTestingTNewCommandInitializer) *CommandInitializer { + mock := &CommandInitializer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/configuration/ConfigurationHolder.go b/mocks/configuration/ConfigurationHolder.go deleted file mode 100644 index f6475bb1..00000000 --- a/mocks/configuration/ConfigurationHolder.go +++ /dev/null @@ -1,38 +0,0 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. - -package mocks - -import ( - mock "github.com/stretchr/testify/mock" - zap "go.uber.org/zap" -) - -// ConfigurationHolder is an autogenerated mock type for the ConfigurationHolder type -type ConfigurationHolder struct { - mock.Mock -} - -// Log provides a mock function with given fields: logger -func (_m *ConfigurationHolder) Log(logger *zap.Logger) { - _m.Called(logger) -} - -// Reload provides a mock function with given fields: -func (_m *ConfigurationHolder) Reload() { - _m.Called() -} - -type mockConstructorTestingTNewConfigurationHolder interface { - mock.TestingT - Cleanup(func()) -} - -// NewConfigurationHolder creates a new instance of ConfigurationHolder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewConfigurationHolder(t mockConstructorTestingTNewConfigurationHolder) *ConfigurationHolder { - mock := &ConfigurationHolder{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/mocks/configuration/DatabaseConfiguration.go b/mocks/configuration/DatabaseConfiguration.go index a210f186..51f0f487 100644 --- a/mocks/configuration/DatabaseConfiguration.go +++ b/mocks/configuration/DatabaseConfiguration.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks diff --git a/mocks/configuration/HTTPServerConfiguration.go b/mocks/configuration/HTTPServerConfiguration.go index 77cb41e5..ffb21116 100644 --- a/mocks/configuration/HTTPServerConfiguration.go +++ b/mocks/configuration/HTTPServerConfiguration.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks @@ -15,6 +15,20 @@ type HTTPServerConfiguration struct { mock.Mock } +// GetAddr provides a mock function with given fields: +func (_m *HTTPServerConfiguration) GetAddr() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // GetHost provides a mock function with given fields: func (_m *HTTPServerConfiguration) GetHost() string { ret := _m.Called() diff --git a/mocks/configuration/Holder.go b/mocks/configuration/Holder.go new file mode 100644 index 00000000..4dfc6188 --- /dev/null +++ b/mocks/configuration/Holder.go @@ -0,0 +1,38 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + zap "go.uber.org/zap" +) + +// Holder is an autogenerated mock type for the Holder type +type Holder struct { + mock.Mock +} + +// Log provides a mock function with given fields: logger +func (_m *Holder) Log(logger *zap.Logger) { + _m.Called(logger) +} + +// Reload provides a mock function with given fields: +func (_m *Holder) Reload() { + _m.Called() +} + +type mockConstructorTestingTNewHolder interface { + mock.TestingT + Cleanup(func()) +} + +// NewHolder creates a new instance of Holder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewHolder(t mockConstructorTestingTNewHolder) *Holder { + mock := &Holder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/configuration/InitializationConfiguration.go b/mocks/configuration/InitializationConfiguration.go index 58bdec3b..303c62b7 100644 --- a/mocks/configuration/InitializationConfiguration.go +++ b/mocks/configuration/InitializationConfiguration.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks diff --git a/mocks/configuration/KeySetter.go b/mocks/configuration/KeySetter.go new file mode 100644 index 00000000..05885cc0 --- /dev/null +++ b/mocks/configuration/KeySetter.go @@ -0,0 +1,44 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + configuration "github.com/ditrit/badaas/configuration" + mock "github.com/stretchr/testify/mock" + + verdeter "github.com/ditrit/verdeter" +) + +// KeySetter is an autogenerated mock type for the KeySetter type +type KeySetter struct { + mock.Mock +} + +// Set provides a mock function with given fields: command, key +func (_m *KeySetter) Set(command *verdeter.VerdeterCommand, key configuration.KeyDefinition) error { + ret := _m.Called(command, key) + + var r0 error + if rf, ok := ret.Get(0).(func(*verdeter.VerdeterCommand, configuration.KeyDefinition) error); ok { + r0 = rf(command, key) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewKeySetter interface { + mock.TestingT + Cleanup(func()) +} + +// NewKeySetter creates a new instance of KeySetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewKeySetter(t mockConstructorTestingTNewKeySetter) *KeySetter { + mock := &KeySetter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/configuration/LoggerConfiguration.go b/mocks/configuration/LoggerConfiguration.go index 2115ec7e..f2687e21 100644 --- a/mocks/configuration/LoggerConfiguration.go +++ b/mocks/configuration/LoggerConfiguration.go @@ -1,9 +1,13 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks import ( mock "github.com/stretchr/testify/mock" + logger "gorm.io/gorm/logger" + + time "time" + zap "go.uber.org/zap" ) @@ -12,6 +16,48 @@ type LoggerConfiguration struct { mock.Mock } +// GetDisableStacktrace provides a mock function with given fields: +func (_m *LoggerConfiguration) GetDisableStacktrace() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// GetIgnoreRecordNotFoundError provides a mock function with given fields: +func (_m *LoggerConfiguration) GetIgnoreRecordNotFoundError() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// GetLogLevel provides a mock function with given fields: +func (_m *LoggerConfiguration) GetLogLevel() logger.LogLevel { + ret := _m.Called() + + var r0 logger.LogLevel + if rf, ok := ret.Get(0).(func() logger.LogLevel); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(logger.LogLevel) + } + + return r0 +} + // GetMode provides a mock function with given fields: func (_m *LoggerConfiguration) GetMode() string { ret := _m.Called() @@ -26,6 +72,20 @@ func (_m *LoggerConfiguration) GetMode() string { return r0 } +// GetParameterizedQueries provides a mock function with given fields: +func (_m *LoggerConfiguration) GetParameterizedQueries() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // GetRequestTemplate provides a mock function with given fields: func (_m *LoggerConfiguration) GetRequestTemplate() string { ret := _m.Called() @@ -40,9 +100,37 @@ func (_m *LoggerConfiguration) GetRequestTemplate() string { return r0 } -// Log provides a mock function with given fields: logger -func (_m *LoggerConfiguration) Log(logger *zap.Logger) { - _m.Called(logger) +// GetSlowQueryThreshold provides a mock function with given fields: +func (_m *LoggerConfiguration) GetSlowQueryThreshold() time.Duration { + ret := _m.Called() + + var r0 time.Duration + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + return r0 +} + +// GetSlowTransactionThreshold provides a mock function with given fields: +func (_m *LoggerConfiguration) GetSlowTransactionThreshold() time.Duration { + ret := _m.Called() + + var r0 time.Duration + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + return r0 +} + +// Log provides a mock function with given fields: _a0 +func (_m *LoggerConfiguration) Log(_a0 *zap.Logger) { + _m.Called(_a0) } // Reload provides a mock function with given fields: diff --git a/mocks/configuration/PaginationConfiguration.go b/mocks/configuration/PaginationConfiguration.go index 9146b875..7db54091 100644 --- a/mocks/configuration/PaginationConfiguration.go +++ b/mocks/configuration/PaginationConfiguration.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks diff --git a/mocks/configuration/SessionConfiguration.go b/mocks/configuration/SessionConfiguration.go index 2d5e9ee7..e107947d 100644 --- a/mocks/configuration/SessionConfiguration.go +++ b/mocks/configuration/SessionConfiguration.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks diff --git a/mocks/controllers/BasicAuthentificationController.go b/mocks/controllers/BasicAuthenticationController.go similarity index 53% rename from mocks/controllers/BasicAuthentificationController.go rename to mocks/controllers/BasicAuthenticationController.go index 19092bfa..5bf3c0b9 100644 --- a/mocks/controllers/BasicAuthentificationController.go +++ b/mocks/controllers/BasicAuthenticationController.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks @@ -9,16 +9,20 @@ import ( mock "github.com/stretchr/testify/mock" ) -// BasicAuthentificationController is an autogenerated mock type for the BasicAuthentificationController type -type BasicAuthentificationController struct { +// BasicAuthenticationController is an autogenerated mock type for the BasicAuthenticationController type +type BasicAuthenticationController struct { mock.Mock } // BasicLoginHandler provides a mock function with given fields: _a0, _a1 -func (_m *BasicAuthentificationController) BasicLoginHandler(_a0 http.ResponseWriter, _a1 *http.Request) (interface{}, httperrors.HTTPError) { +func (_m *BasicAuthenticationController) BasicLoginHandler(_a0 http.ResponseWriter, _a1 *http.Request) (interface{}, httperrors.HTTPError) { ret := _m.Called(_a0, _a1) var r0 interface{} + var r1 httperrors.HTTPError + if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) (interface{}, httperrors.HTTPError)); ok { + return rf(_a0, _a1) + } if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) interface{}); ok { r0 = rf(_a0, _a1) } else { @@ -27,7 +31,6 @@ func (_m *BasicAuthentificationController) BasicLoginHandler(_a0 http.ResponseWr } } - var r1 httperrors.HTTPError if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) httperrors.HTTPError); ok { r1 = rf(_a0, _a1) } else { @@ -40,10 +43,14 @@ func (_m *BasicAuthentificationController) BasicLoginHandler(_a0 http.ResponseWr } // Logout provides a mock function with given fields: _a0, _a1 -func (_m *BasicAuthentificationController) Logout(_a0 http.ResponseWriter, _a1 *http.Request) (interface{}, httperrors.HTTPError) { +func (_m *BasicAuthenticationController) Logout(_a0 http.ResponseWriter, _a1 *http.Request) (interface{}, httperrors.HTTPError) { ret := _m.Called(_a0, _a1) var r0 interface{} + var r1 httperrors.HTTPError + if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) (interface{}, httperrors.HTTPError)); ok { + return rf(_a0, _a1) + } if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) interface{}); ok { r0 = rf(_a0, _a1) } else { @@ -52,7 +59,6 @@ func (_m *BasicAuthentificationController) Logout(_a0 http.ResponseWriter, _a1 * } } - var r1 httperrors.HTTPError if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) httperrors.HTTPError); ok { r1 = rf(_a0, _a1) } else { @@ -64,14 +70,14 @@ func (_m *BasicAuthentificationController) Logout(_a0 http.ResponseWriter, _a1 * return r0, r1 } -type mockConstructorTestingTNewBasicAuthentificationController interface { +type mockConstructorTestingTNewBasicAuthenticationController interface { mock.TestingT Cleanup(func()) } -// NewBasicAuthentificationController creates a new instance of BasicAuthentificationController. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewBasicAuthentificationController(t mockConstructorTestingTNewBasicAuthentificationController) *BasicAuthentificationController { - mock := &BasicAuthentificationController{} +// NewBasicAuthenticationController creates a new instance of BasicAuthenticationController. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewBasicAuthenticationController(t mockConstructorTestingTNewBasicAuthenticationController) *BasicAuthenticationController { + mock := &BasicAuthenticationController{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/mocks/controllers/InformationController.go b/mocks/controllers/InformationController.go index 51eeffef..4a82f694 100644 --- a/mocks/controllers/InformationController.go +++ b/mocks/controllers/InformationController.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks @@ -19,6 +19,10 @@ func (_m *InformationController) Info(response http.ResponseWriter, r *http.Requ ret := _m.Called(response, r) var r0 interface{} + var r1 httperrors.HTTPError + if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) (interface{}, httperrors.HTTPError)); ok { + return rf(response, r) + } if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) interface{}); ok { r0 = rf(response, r) } else { @@ -27,7 +31,6 @@ func (_m *InformationController) Info(response http.ResponseWriter, r *http.Requ } } - var r1 httperrors.HTTPError if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) httperrors.HTTPError); ok { r1 = rf(response, r) } else { diff --git a/mocks/httperrors/HTTPError.go b/mocks/httperrors/HTTPError.go index cbe71e52..3bae2ab3 100644 --- a/mocks/httperrors/HTTPError.go +++ b/mocks/httperrors/HTTPError.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks diff --git a/mocks/orm/condition/Condition.go b/mocks/orm/condition/Condition.go new file mode 100644 index 00000000..9611e9ce --- /dev/null +++ b/mocks/orm/condition/Condition.go @@ -0,0 +1,49 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + model "github.com/ditrit/badaas/orm/model" + mock "github.com/stretchr/testify/mock" + + query "github.com/ditrit/badaas/orm/query" +) + +// Condition is an autogenerated mock type for the Condition type +type Condition[T model.Model] struct { + mock.Mock +} + +// ApplyTo provides a mock function with given fields: _a0, _a1 +func (_m *Condition[T]) ApplyTo(_a0 *query.GormQuery, _a1 query.Table) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(*query.GormQuery, query.Table) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// InterfaceVerificationMethod provides a mock function with given fields: _a0 +func (_m *Condition[T]) InterfaceVerificationMethod(_a0 T) { + _m.Called(_a0) +} + +type mockConstructorTestingTNewCondition interface { + mock.TestingT + Cleanup(func()) +} + +// NewCondition creates a new instance of Condition. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewCondition[T model.Model](t mockConstructorTestingTNewCondition) *Condition[T] { + mock := &Condition[T]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/orm/condition/JoinCondition.go b/mocks/orm/condition/JoinCondition.go new file mode 100644 index 00000000..1c96ff80 --- /dev/null +++ b/mocks/orm/condition/JoinCondition.go @@ -0,0 +1,77 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + model "github.com/ditrit/badaas/orm/model" + mock "github.com/stretchr/testify/mock" + + query "github.com/ditrit/badaas/orm/query" +) + +// JoinCondition is an autogenerated mock type for the JoinCondition type +type JoinCondition[T model.Model] struct { + mock.Mock +} + +// ApplyTo provides a mock function with given fields: _a0, _a1 +func (_m *JoinCondition[T]) ApplyTo(_a0 *query.GormQuery, _a1 query.Table) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(*query.GormQuery, query.Table) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// InterfaceVerificationMethod provides a mock function with given fields: _a0 +func (_m *JoinCondition[T]) InterfaceVerificationMethod(_a0 T) { + _m.Called(_a0) +} + +// makesFilter provides a mock function with given fields: +func (_m *JoinCondition[T]) makesFilter() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// makesPreload provides a mock function with given fields: +func (_m *JoinCondition[T]) makesPreload() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +type mockConstructorTestingTNewJoinCondition interface { + mock.TestingT + Cleanup(func()) +} + +// NewJoinCondition creates a new instance of JoinCondition. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewJoinCondition[T model.Model](t mockConstructorTestingTNewJoinCondition) *JoinCondition[T] { + mock := &JoinCondition[T]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/orm/condition/WhereCondition.go b/mocks/orm/condition/WhereCondition.go new file mode 100644 index 00000000..82fae0e1 --- /dev/null +++ b/mocks/orm/condition/WhereCondition.go @@ -0,0 +1,96 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + model "github.com/ditrit/badaas/orm/model" + mock "github.com/stretchr/testify/mock" + + query "github.com/ditrit/badaas/orm/query" +) + +// WhereCondition is an autogenerated mock type for the WhereCondition type +type WhereCondition[T model.Model] struct { + mock.Mock +} + +// AffectsDeletedAt provides a mock function with given fields: +func (_m *WhereCondition[T]) AffectsDeletedAt() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ApplyTo provides a mock function with given fields: _a0, _a1 +func (_m *WhereCondition[T]) ApplyTo(_a0 *query.GormQuery, _a1 query.Table) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(*query.GormQuery, query.Table) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetSQL provides a mock function with given fields: _a0, table +func (_m *WhereCondition[T]) GetSQL(_a0 *query.GormQuery, table query.Table) (string, []interface{}, error) { + ret := _m.Called(_a0, table) + + var r0 string + var r1 []interface{} + var r2 error + if rf, ok := ret.Get(0).(func(*query.GormQuery, query.Table) (string, []interface{}, error)); ok { + return rf(_a0, table) + } + if rf, ok := ret.Get(0).(func(*query.GormQuery, query.Table) string); ok { + r0 = rf(_a0, table) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(*query.GormQuery, query.Table) []interface{}); ok { + r1 = rf(_a0, table) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]interface{}) + } + } + + if rf, ok := ret.Get(2).(func(*query.GormQuery, query.Table) error); ok { + r2 = rf(_a0, table) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// InterfaceVerificationMethod provides a mock function with given fields: _a0 +func (_m *WhereCondition[T]) InterfaceVerificationMethod(_a0 T) { + _m.Called(_a0) +} + +type mockConstructorTestingTNewWhereCondition interface { + mock.TestingT + Cleanup(func()) +} + +// NewWhereCondition creates a new instance of WhereCondition. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewWhereCondition[T model.Model](t mockConstructorTestingTNewWhereCondition) *WhereCondition[T] { + mock := &WhereCondition[T]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/orm/logger/Interface.go b/mocks/orm/logger/Interface.go new file mode 100644 index 00000000..fed65ad1 --- /dev/null +++ b/mocks/orm/logger/Interface.go @@ -0,0 +1,101 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + gormlogger "gorm.io/gorm/logger" + + logger "github.com/ditrit/badaas/orm/logger" + + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// Interface is an autogenerated mock type for the Interface type +type Interface struct { + mock.Mock +} + +// Error provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Interface) Error(_a0 context.Context, _a1 string, _a2 ...interface{}) { + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _a2...) + _m.Called(_ca...) +} + +// Info provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Interface) Info(_a0 context.Context, _a1 string, _a2 ...interface{}) { + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _a2...) + _m.Called(_ca...) +} + +// LogMode provides a mock function with given fields: _a0 +func (_m *Interface) LogMode(_a0 gormlogger.LogLevel) gormlogger.Interface { + ret := _m.Called(_a0) + + var r0 gormlogger.Interface + if rf, ok := ret.Get(0).(func(gormlogger.LogLevel) gormlogger.Interface); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(gormlogger.Interface) + } + } + + return r0 +} + +// ToLogMode provides a mock function with given fields: _a0 +func (_m *Interface) ToLogMode(_a0 gormlogger.LogLevel) logger.Interface { + ret := _m.Called(_a0) + + var r0 logger.Interface + if rf, ok := ret.Get(0).(func(gormlogger.LogLevel) logger.Interface); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(logger.Interface) + } + } + + return r0 +} + +// Trace provides a mock function with given fields: ctx, begin, fc, err +func (_m *Interface) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { + _m.Called(ctx, begin, fc, err) +} + +// TraceTransaction provides a mock function with given fields: ctx, begin +func (_m *Interface) TraceTransaction(ctx context.Context, begin time.Time) { + _m.Called(ctx, begin) +} + +// Warn provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Interface) Warn(_a0 context.Context, _a1 string, _a2 ...interface{}) { + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _a2...) + _m.Called(_ca...) +} + +type mockConstructorTestingTNewInterface interface { + mock.TestingT + Cleanup(func()) +} + +// NewInterface creates a new instance of Interface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewInterface(t mockConstructorTestingTNewInterface) *Interface { + mock := &Interface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/orm/model/ID.go b/mocks/orm/model/ID.go new file mode 100644 index 00000000..11bdedeb --- /dev/null +++ b/mocks/orm/model/ID.go @@ -0,0 +1,39 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// ID is an autogenerated mock type for the ID type +type ID struct { + mock.Mock +} + +// IsNil provides a mock function with given fields: +func (_m *ID) IsNil() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +type mockConstructorTestingTNewID interface { + mock.TestingT + Cleanup(func()) +} + +// NewID creates a new instance of ID. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewID(t mockConstructorTestingTNewID) *ID { + mock := &ID{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/orm/model/Model.go b/mocks/orm/model/Model.go new file mode 100644 index 00000000..ca967b67 --- /dev/null +++ b/mocks/orm/model/Model.go @@ -0,0 +1,39 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// Model is an autogenerated mock type for the Model type +type Model struct { + mock.Mock +} + +// IsLoaded provides a mock function with given fields: +func (_m *Model) IsLoaded() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +type mockConstructorTestingTNewModel interface { + mock.TestingT + Cleanup(func()) +} + +// NewModel creates a new instance of Model. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewModel(t mockConstructorTestingTNewModel) *Model { + mock := &Model{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/orm/operator/DynamicOperator.go b/mocks/orm/operator/DynamicOperator.go new file mode 100644 index 00000000..093c74f4 --- /dev/null +++ b/mocks/orm/operator/DynamicOperator.go @@ -0,0 +1,84 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + operator "github.com/ditrit/badaas/orm/operator" + mock "github.com/stretchr/testify/mock" + + query "github.com/ditrit/badaas/orm/query" +) + +// DynamicOperator is an autogenerated mock type for the DynamicOperator type +type DynamicOperator[T interface{}] struct { + mock.Mock +} + +// InterfaceVerificationMethod provides a mock function with given fields: _a0 +func (_m *DynamicOperator[T]) InterfaceVerificationMethod(_a0 T) { + _m.Called(_a0) +} + +// SelectJoin provides a mock function with given fields: valueNumber, joinNumber +func (_m *DynamicOperator[T]) SelectJoin(valueNumber uint, joinNumber uint) operator.DynamicOperator[T] { + ret := _m.Called(valueNumber, joinNumber) + + var r0 operator.DynamicOperator[T] + if rf, ok := ret.Get(0).(func(uint, uint) operator.DynamicOperator[T]); ok { + r0 = rf(valueNumber, joinNumber) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(operator.DynamicOperator[T]) + } + } + + return r0 +} + +// ToSQL provides a mock function with given fields: _a0, columnName +func (_m *DynamicOperator[T]) ToSQL(_a0 *query.GormQuery, columnName string) (string, []interface{}, error) { + ret := _m.Called(_a0, columnName) + + var r0 string + var r1 []interface{} + var r2 error + if rf, ok := ret.Get(0).(func(*query.GormQuery, string) (string, []interface{}, error)); ok { + return rf(_a0, columnName) + } + if rf, ok := ret.Get(0).(func(*query.GormQuery, string) string); ok { + r0 = rf(_a0, columnName) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(*query.GormQuery, string) []interface{}); ok { + r1 = rf(_a0, columnName) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]interface{}) + } + } + + if rf, ok := ret.Get(2).(func(*query.GormQuery, string) error); ok { + r2 = rf(_a0, columnName) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +type mockConstructorTestingTNewDynamicOperator interface { + mock.TestingT + Cleanup(func()) +} + +// NewDynamicOperator creates a new instance of DynamicOperator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewDynamicOperator[T interface{}](t mockConstructorTestingTNewDynamicOperator) *DynamicOperator[T] { + mock := &DynamicOperator[T]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/orm/operator/Operator.go b/mocks/orm/operator/Operator.go new file mode 100644 index 00000000..f3c78e8f --- /dev/null +++ b/mocks/orm/operator/Operator.go @@ -0,0 +1,67 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + + query "github.com/ditrit/badaas/orm/query" +) + +// Operator is an autogenerated mock type for the Operator type +type Operator[T interface{}] struct { + mock.Mock +} + +// InterfaceVerificationMethod provides a mock function with given fields: _a0 +func (_m *Operator[T]) InterfaceVerificationMethod(_a0 T) { + _m.Called(_a0) +} + +// ToSQL provides a mock function with given fields: _a0, columnName +func (_m *Operator[T]) ToSQL(_a0 *query.GormQuery, columnName string) (string, []interface{}, error) { + ret := _m.Called(_a0, columnName) + + var r0 string + var r1 []interface{} + var r2 error + if rf, ok := ret.Get(0).(func(*query.GormQuery, string) (string, []interface{}, error)); ok { + return rf(_a0, columnName) + } + if rf, ok := ret.Get(0).(func(*query.GormQuery, string) string); ok { + r0 = rf(_a0, columnName) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(*query.GormQuery, string) []interface{}); ok { + r1 = rf(_a0, columnName) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]interface{}) + } + } + + if rf, ok := ret.Get(2).(func(*query.GormQuery, string) error); ok { + r2 = rf(_a0, columnName) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +type mockConstructorTestingTNewOperator interface { + mock.TestingT + Cleanup(func()) +} + +// NewOperator creates a new instance of Operator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewOperator[T interface{}](t mockConstructorTestingTNewOperator) *Operator[T] { + mock := &Operator[T]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/orm/query/IFieldIdentifier.go b/mocks/orm/query/IFieldIdentifier.go new file mode 100644 index 00000000..c2869d92 --- /dev/null +++ b/mocks/orm/query/IFieldIdentifier.go @@ -0,0 +1,88 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + query "github.com/ditrit/badaas/orm/query" + mock "github.com/stretchr/testify/mock" + + reflect "reflect" +) + +// IFieldIdentifier is an autogenerated mock type for the IFieldIdentifier type +type IFieldIdentifier struct { + mock.Mock +} + +// ColumnName provides a mock function with given fields: _a0, table +func (_m *IFieldIdentifier) ColumnName(_a0 *query.GormQuery, table query.Table) string { + ret := _m.Called(_a0, table) + + var r0 string + if rf, ok := ret.Get(0).(func(*query.GormQuery, query.Table) string); ok { + r0 = rf(_a0, table) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// ColumnSQL provides a mock function with given fields: _a0, table +func (_m *IFieldIdentifier) ColumnSQL(_a0 *query.GormQuery, table query.Table) string { + ret := _m.Called(_a0, table) + + var r0 string + if rf, ok := ret.Get(0).(func(*query.GormQuery, query.Table) string); ok { + r0 = rf(_a0, table) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// FieldName provides a mock function with given fields: +func (_m *IFieldIdentifier) FieldName() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// GetModelType provides a mock function with given fields: +func (_m *IFieldIdentifier) GetModelType() reflect.Type { + ret := _m.Called() + + var r0 reflect.Type + if rf, ok := ret.Get(0).(func() reflect.Type); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(reflect.Type) + } + } + + return r0 +} + +type mockConstructorTestingTNewIFieldIdentifier interface { + mock.TestingT + Cleanup(func()) +} + +// NewIFieldIdentifier creates a new instance of IFieldIdentifier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewIFieldIdentifier(t mockConstructorTestingTNewIFieldIdentifier) *IFieldIdentifier { + mock := &IFieldIdentifier{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/persistence/models/Tabler.go b/mocks/persistence/models/Tabler.go deleted file mode 100644 index cbc8e129..00000000 --- a/mocks/persistence/models/Tabler.go +++ /dev/null @@ -1,39 +0,0 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. - -package mocks - -import mock "github.com/stretchr/testify/mock" - -// Tabler is an autogenerated mock type for the Tabler type -type Tabler struct { - mock.Mock -} - -// TableName provides a mock function with given fields: -func (_m *Tabler) TableName() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -type mockConstructorTestingTNewTabler interface { - mock.TestingT - Cleanup(func()) -} - -// NewTabler creates a new instance of Tabler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewTabler(t mockConstructorTestingTNewTabler) *Tabler { - mock := &Tabler{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/mocks/persistence/pagination/Paginator.go b/mocks/persistence/pagination/Paginator.go deleted file mode 100644 index dabbbf23..00000000 --- a/mocks/persistence/pagination/Paginator.go +++ /dev/null @@ -1,53 +0,0 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. - -package mocks - -import mock "github.com/stretchr/testify/mock" - -// Paginator is an autogenerated mock type for the Paginator type -type Paginator struct { - mock.Mock -} - -// Limit provides a mock function with given fields: -func (_m *Paginator) Limit() uint { - ret := _m.Called() - - var r0 uint - if rf, ok := ret.Get(0).(func() uint); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(uint) - } - - return r0 -} - -// Offset provides a mock function with given fields: -func (_m *Paginator) Offset() uint { - ret := _m.Called() - - var r0 uint - if rf, ok := ret.Get(0).(func() uint); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(uint) - } - - return r0 -} - -type mockConstructorTestingTNewPaginator interface { - mock.TestingT - Cleanup(func()) -} - -// NewPaginator creates a new instance of Paginator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewPaginator(t mockConstructorTestingTNewPaginator) *Paginator { - mock := &Paginator{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/mocks/persistence/repository/CRUD.go b/mocks/persistence/repository/CRUD.go new file mode 100644 index 00000000..a63f1cca --- /dev/null +++ b/mocks/persistence/repository/CRUD.go @@ -0,0 +1,166 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + condition "github.com/ditrit/badaas/orm/condition" + gorm "gorm.io/gorm" + + mock "github.com/stretchr/testify/mock" + + model "github.com/ditrit/badaas/orm/model" +) + +// CRUD is an autogenerated mock type for the CRUD type +type CRUD[T model.Model, ID model.ID] struct { + mock.Mock +} + +// Create provides a mock function with given fields: tx, entity +func (_m *CRUD[T, ID]) Create(tx *gorm.DB, entity *T) error { + ret := _m.Called(tx, entity) + + var r0 error + if rf, ok := ret.Get(0).(func(*gorm.DB, *T) error); ok { + r0 = rf(tx, entity) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Delete provides a mock function with given fields: tx, entity +func (_m *CRUD[T, ID]) Delete(tx *gorm.DB, entity *T) error { + ret := _m.Called(tx, entity) + + var r0 error + if rf, ok := ret.Get(0).(func(*gorm.DB, *T) error); ok { + r0 = rf(tx, entity) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Find provides a mock function with given fields: tx, conditions +func (_m *CRUD[T, ID]) Find(tx *gorm.DB, conditions ...condition.Condition[T]) ([]*T, error) { + _va := make([]interface{}, len(conditions)) + for _i := range conditions { + _va[_i] = conditions[_i] + } + var _ca []interface{} + _ca = append(_ca, tx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []*T + var r1 error + if rf, ok := ret.Get(0).(func(*gorm.DB, ...condition.Condition[T]) ([]*T, error)); ok { + return rf(tx, conditions...) + } + if rf, ok := ret.Get(0).(func(*gorm.DB, ...condition.Condition[T]) []*T); ok { + r0 = rf(tx, conditions...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*T) + } + } + + if rf, ok := ret.Get(1).(func(*gorm.DB, ...condition.Condition[T]) error); ok { + r1 = rf(tx, conditions...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindOne provides a mock function with given fields: tx, conditions +func (_m *CRUD[T, ID]) FindOne(tx *gorm.DB, conditions ...condition.Condition[T]) (*T, error) { + _va := make([]interface{}, len(conditions)) + for _i := range conditions { + _va[_i] = conditions[_i] + } + var _ca []interface{} + _ca = append(_ca, tx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *T + var r1 error + if rf, ok := ret.Get(0).(func(*gorm.DB, ...condition.Condition[T]) (*T, error)); ok { + return rf(tx, conditions...) + } + if rf, ok := ret.Get(0).(func(*gorm.DB, ...condition.Condition[T]) *T); ok { + r0 = rf(tx, conditions...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*T) + } + } + + if rf, ok := ret.Get(1).(func(*gorm.DB, ...condition.Condition[T]) error); ok { + r1 = rf(tx, conditions...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetByID provides a mock function with given fields: tx, id +func (_m *CRUD[T, ID]) GetByID(tx *gorm.DB, id ID) (*T, error) { + ret := _m.Called(tx, id) + + var r0 *T + var r1 error + if rf, ok := ret.Get(0).(func(*gorm.DB, ID) (*T, error)); ok { + return rf(tx, id) + } + if rf, ok := ret.Get(0).(func(*gorm.DB, ID) *T); ok { + r0 = rf(tx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*T) + } + } + + if rf, ok := ret.Get(1).(func(*gorm.DB, ID) error); ok { + r1 = rf(tx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Save provides a mock function with given fields: tx, entity +func (_m *CRUD[T, ID]) Save(tx *gorm.DB, entity *T) error { + ret := _m.Called(tx, entity) + + var r0 error + if rf, ok := ret.Get(0).(func(*gorm.DB, *T) error); ok { + r0 = rf(tx, entity) + } else { + r0 = ret.Error(0) + }// Get the list of models that match "conditions" inside transaction "tx" + + return r0 +} + +type mockConstructorTestingTNewCRUD interface { + mock.TestingT + Cleanup(func()) +} + +// NewCRUD creates a new instance of CRUD. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewCRUD[T model.Model, ID model.ID](t mockConstructorTestingTNewCRUD) *CRUD[T, ID] { + mock := &CRUD[T, ID]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/persistence/repository/CRUDRepository.go b/mocks/persistence/repository/CRUDRepository.go deleted file mode 100644 index 31f127b1..00000000 --- a/mocks/persistence/repository/CRUDRepository.go +++ /dev/null @@ -1,207 +0,0 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. - -package mocks - -import ( - httperrors "github.com/ditrit/badaas/httperrors" - mock "github.com/stretchr/testify/mock" - - models "github.com/ditrit/badaas/persistence/models" - - pagination "github.com/ditrit/badaas/persistence/pagination" - - repository "github.com/ditrit/badaas/persistence/repository" - - squirrel "github.com/Masterminds/squirrel" -) - -// CRUDRepository is an autogenerated mock type for the CRUDRepository type -type CRUDRepository[T models.Tabler, ID interface{}] struct { - mock.Mock -} - -// Count provides a mock function with given fields: _a0 -func (_m *CRUDRepository[T, ID]) Count(_a0 squirrel.Sqlizer) (uint, httperrors.HTTPError) { - ret := _m.Called(_a0) - - var r0 uint - if rf, ok := ret.Get(0).(func(squirrel.Sqlizer) uint); ok { - r0 = rf(_a0) - } else { - r0 = ret.Get(0).(uint) - } - - var r1 httperrors.HTTPError - if rf, ok := ret.Get(1).(func(squirrel.Sqlizer) httperrors.HTTPError); ok { - r1 = rf(_a0) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(httperrors.HTTPError) - } - } - - return r0, r1 -} - -// Create provides a mock function with given fields: _a0 -func (_m *CRUDRepository[T, ID]) Create(_a0 *T) httperrors.HTTPError { - ret := _m.Called(_a0) - - var r0 httperrors.HTTPError - if rf, ok := ret.Get(0).(func(*T) httperrors.HTTPError); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(httperrors.HTTPError) - } - } - - return r0 -} - -// Delete provides a mock function with given fields: _a0 -func (_m *CRUDRepository[T, ID]) Delete(_a0 *T) httperrors.HTTPError { - ret := _m.Called(_a0) - - var r0 httperrors.HTTPError - if rf, ok := ret.Get(0).(func(*T) httperrors.HTTPError); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(httperrors.HTTPError) - } - } - - return r0 -} - -// Find provides a mock function with given fields: _a0, _a1, _a2 -func (_m *CRUDRepository[T, ID]) Find(_a0 squirrel.Sqlizer, _a1 pagination.Paginator, _a2 repository.SortOption) (*pagination.Page[T], httperrors.HTTPError) { - ret := _m.Called(_a0, _a1, _a2) - - var r0 *pagination.Page[T] - if rf, ok := ret.Get(0).(func(squirrel.Sqlizer, pagination.Paginator, repository.SortOption) *pagination.Page[T]); ok { - r0 = rf(_a0, _a1, _a2) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*pagination.Page[T]) - } - } - - var r1 httperrors.HTTPError - if rf, ok := ret.Get(1).(func(squirrel.Sqlizer, pagination.Paginator, repository.SortOption) httperrors.HTTPError); ok { - r1 = rf(_a0, _a1, _a2) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(httperrors.HTTPError) - } - } - - return r0, r1 -} - -// GetAll provides a mock function with given fields: _a0 -func (_m *CRUDRepository[T, ID]) GetAll(_a0 repository.SortOption) ([]*T, httperrors.HTTPError) { - ret := _m.Called(_a0) - - var r0 []*T - if rf, ok := ret.Get(0).(func(repository.SortOption) []*T); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*T) - } - } - - var r1 httperrors.HTTPError - if rf, ok := ret.Get(1).(func(repository.SortOption) httperrors.HTTPError); ok { - r1 = rf(_a0) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(httperrors.HTTPError) - } - } - - return r0, r1 -} - -// GetByID provides a mock function with given fields: _a0 -func (_m *CRUDRepository[T, ID]) GetByID(_a0 ID) (*T, httperrors.HTTPError) { - ret := _m.Called(_a0) - - var r0 *T - if rf, ok := ret.Get(0).(func(ID) *T); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*T) - } - } - - var r1 httperrors.HTTPError - if rf, ok := ret.Get(1).(func(ID) httperrors.HTTPError); ok { - r1 = rf(_a0) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(httperrors.HTTPError) - } - } - - return r0, r1 -} - -// Save provides a mock function with given fields: _a0 -func (_m *CRUDRepository[T, ID]) Save(_a0 *T) httperrors.HTTPError { - ret := _m.Called(_a0) - - var r0 httperrors.HTTPError - if rf, ok := ret.Get(0).(func(*T) httperrors.HTTPError); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(httperrors.HTTPError) - } - } - - return r0 -} - -// Transaction provides a mock function with given fields: fn -func (_m *CRUDRepository[T, ID]) Transaction(fn func(repository.CRUDRepository[T, ID]) (interface{}, error)) (interface{}, httperrors.HTTPError) { - ret := _m.Called(fn) - - var r0 interface{} - if rf, ok := ret.Get(0).(func(func(repository.CRUDRepository[T, ID]) (interface{}, error)) interface{}); ok { - r0 = rf(fn) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(interface{}) - } - } - - var r1 httperrors.HTTPError - if rf, ok := ret.Get(1).(func(func(repository.CRUDRepository[T, ID]) (interface{}, error)) httperrors.HTTPError); ok { - r1 = rf(fn) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(httperrors.HTTPError) - } - } - - return r0, r1 -} - -type mockConstructorTestingTNewCRUDRepository interface { - mock.TestingT - Cleanup(func()) -} - -// NewCRUDRepository creates a new instance of CRUDRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewCRUDRepository[T models.Tabler, ID interface{}](t mockConstructorTestingTNewCRUDRepository) *CRUDRepository[T, ID] { - mock := &CRUDRepository[T, ID]{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/mocks/persistence/repository/SortOption.go b/mocks/persistence/repository/SortOption.go deleted file mode 100644 index d2e10e83..00000000 --- a/mocks/persistence/repository/SortOption.go +++ /dev/null @@ -1,53 +0,0 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. - -package mocks - -import mock "github.com/stretchr/testify/mock" - -// SortOption is an autogenerated mock type for the SortOption type -type SortOption struct { - mock.Mock -} - -// Column provides a mock function with given fields: -func (_m *SortOption) Column() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// Desc provides a mock function with given fields: -func (_m *SortOption) Desc() bool { - ret := _m.Called() - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -type mockConstructorTestingTNewSortOption interface { - mock.TestingT - Cleanup(func()) -} - -// NewSortOption creates a new instance of SortOption. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSortOption(t mockConstructorTestingTNewSortOption) *SortOption { - mock := &SortOption{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/mocks/router/middlewares/AuthenticationMiddleware.go b/mocks/router/middlewares/AuthenticationMiddleware.go index a0a4053f..77b8fe2d 100644 --- a/mocks/router/middlewares/AuthenticationMiddleware.go +++ b/mocks/router/middlewares/AuthenticationMiddleware.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks diff --git a/mocks/router/middlewares/JSONController.go b/mocks/router/middlewares/JSONController.go index b1ad06cb..5fe7aa5a 100644 --- a/mocks/router/middlewares/JSONController.go +++ b/mocks/router/middlewares/JSONController.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks diff --git a/mocks/router/middlewares/JSONHandler.go b/mocks/router/middlewares/JSONHandler.go index 98dd567b..37a072cb 100644 --- a/mocks/router/middlewares/JSONHandler.go +++ b/mocks/router/middlewares/JSONHandler.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks @@ -20,6 +20,10 @@ func (_m *JSONHandler) Execute(w http.ResponseWriter, r *http.Request) (interfac ret := _m.Called(w, r) var r0 interface{} + var r1 httperrors.HTTPError + if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) (interface{}, httperrors.HTTPError)); ok { + return rf(w, r) + } if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) interface{}); ok { r0 = rf(w, r) } else { @@ -28,7 +32,6 @@ func (_m *JSONHandler) Execute(w http.ResponseWriter, r *http.Request) (interfac } } - var r1 httperrors.HTTPError if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) httperrors.HTTPError); ok { r1 = rf(w, r) } else { diff --git a/mocks/router/middlewares/MiddlewareLogger.go b/mocks/router/middlewares/MiddlewareLogger.go index 671b8ca8..f18920a8 100644 --- a/mocks/router/middlewares/MiddlewareLogger.go +++ b/mocks/router/middlewares/MiddlewareLogger.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks diff --git a/mocks/services/sessionservice/SessionService.go b/mocks/services/sessionservice/SessionService.go index 22b49694..44c7f52d 100644 --- a/mocks/services/sessionservice/SessionService.go +++ b/mocks/services/sessionservice/SessionService.go @@ -1,18 +1,16 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks import ( - http "net/http" - httperrors "github.com/ditrit/badaas/httperrors" mock "github.com/stretchr/testify/mock" + model "github.com/ditrit/badaas/orm/model" + models "github.com/ditrit/badaas/persistence/models" sessionservice "github.com/ditrit/badaas/services/sessionservice" - - uuid "github.com/google/uuid" ) // SessionService is an autogenerated mock type for the SessionService type @@ -21,18 +19,21 @@ type SessionService struct { } // IsValid provides a mock function with given fields: sessionUUID -func (_m *SessionService) IsValid(sessionUUID uuid.UUID) (bool, *sessionservice.SessionClaims) { +func (_m *SessionService) IsValid(sessionUUID model.UUID) (bool, *sessionservice.SessionClaims) { ret := _m.Called(sessionUUID) var r0 bool - if rf, ok := ret.Get(0).(func(uuid.UUID) bool); ok { + var r1 *sessionservice.SessionClaims + if rf, ok := ret.Get(0).(func(model.UUID) (bool, *sessionservice.SessionClaims)); ok { + return rf(sessionUUID) + } + if rf, ok := ret.Get(0).(func(model.UUID) bool); ok { r0 = rf(sessionUUID) } else { r0 = ret.Get(0).(bool) } - var r1 *sessionservice.SessionClaims - if rf, ok := ret.Get(1).(func(uuid.UUID) *sessionservice.SessionClaims); ok { + if rf, ok := ret.Get(1).(func(model.UUID) *sessionservice.SessionClaims); ok { r1 = rf(sessionUUID) } else { if ret.Get(1) != nil { @@ -43,29 +44,39 @@ func (_m *SessionService) IsValid(sessionUUID uuid.UUID) (bool, *sessionservice. return r0, r1 } -// LogUserIn provides a mock function with given fields: user, response -func (_m *SessionService) LogUserIn(user *models.User, response http.ResponseWriter) httperrors.HTTPError { - ret := _m.Called(user, response) +// LogUserIn provides a mock function with given fields: user +func (_m *SessionService) LogUserIn(user *models.User) (*models.Session, error) { + ret := _m.Called(user) - var r0 httperrors.HTTPError - if rf, ok := ret.Get(0).(func(*models.User, http.ResponseWriter) httperrors.HTTPError); ok { - r0 = rf(user, response) + var r0 *models.Session + var r1 error + if rf, ok := ret.Get(0).(func(*models.User) (*models.Session, error)); ok { + return rf(user) + } + if rf, ok := ret.Get(0).(func(*models.User) *models.Session); ok { + r0 = rf(user) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(httperrors.HTTPError) + r0 = ret.Get(0).(*models.Session) } } - return r0 + if rf, ok := ret.Get(1).(func(*models.User) error); ok { + r1 = rf(user) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// LogUserOut provides a mock function with given fields: sessionClaims, response -func (_m *SessionService) LogUserOut(sessionClaims *sessionservice.SessionClaims, response http.ResponseWriter) httperrors.HTTPError { - ret := _m.Called(sessionClaims, response) +// LogUserOut provides a mock function with given fields: sessionClaims +func (_m *SessionService) LogUserOut(sessionClaims *sessionservice.SessionClaims) httperrors.HTTPError { + ret := _m.Called(sessionClaims) var r0 httperrors.HTTPError - if rf, ok := ret.Get(0).(func(*sessionservice.SessionClaims, http.ResponseWriter) httperrors.HTTPError); ok { - r0 = rf(sessionClaims, response) + if rf, ok := ret.Get(0).(func(*sessionservice.SessionClaims) httperrors.HTTPError); ok { + r0 = rf(sessionClaims) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(httperrors.HTTPError) @@ -76,11 +87,11 @@ func (_m *SessionService) LogUserOut(sessionClaims *sessionservice.SessionClaims } // RollSession provides a mock function with given fields: _a0 -func (_m *SessionService) RollSession(_a0 uuid.UUID) httperrors.HTTPError { +func (_m *SessionService) RollSession(_a0 model.UUID) httperrors.HTTPError { ret := _m.Called(_a0) var r0 httperrors.HTTPError - if rf, ok := ret.Get(0).(func(uuid.UUID) httperrors.HTTPError); ok { + if rf, ok := ret.Get(0).(func(model.UUID) httperrors.HTTPError); ok { r0 = rf(_a0) } else { if ret.Get(0) != nil { diff --git a/mocks/services/userservice/UserService.go b/mocks/services/userservice/UserService.go index 6ce9b7fa..6ecc1e6b 100644 --- a/mocks/services/userservice/UserService.go +++ b/mocks/services/userservice/UserService.go @@ -1,11 +1,9 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks import ( - httperrors "github.com/ditrit/badaas/httperrors" dto "github.com/ditrit/badaas/persistence/models/dto" - mock "github.com/stretchr/testify/mock" models "github.com/ditrit/badaas/persistence/models" @@ -17,10 +15,14 @@ type UserService struct { } // GetUser provides a mock function with given fields: _a0 -func (_m *UserService) GetUser(_a0 dto.UserLoginDTO) (*models.User, httperrors.HTTPError) { +func (_m *UserService) GetUser(_a0 dto.UserLoginDTO) (*models.User, error) { ret := _m.Called(_a0) var r0 *models.User + var r1 error + if rf, ok := ret.Get(0).(func(dto.UserLoginDTO) (*models.User, error)); ok { + return rf(_a0) + } if rf, ok := ret.Get(0).(func(dto.UserLoginDTO) *models.User); ok { r0 = rf(_a0) } else { @@ -29,13 +31,10 @@ func (_m *UserService) GetUser(_a0 dto.UserLoginDTO) (*models.User, httperrors.H } } - var r1 httperrors.HTTPError - if rf, ok := ret.Get(1).(func(dto.UserLoginDTO) httperrors.HTTPError); ok { + if rf, ok := ret.Get(1).(func(dto.UserLoginDTO) error); ok { r1 = rf(_a0) } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(httperrors.HTTPError) - } + r1 = ret.Error(1) } return r0, r1 @@ -46,6 +45,10 @@ func (_m *UserService) NewUser(username string, email string, password string) ( ret := _m.Called(username, email, password) var r0 *models.User + var r1 error + if rf, ok := ret.Get(0).(func(string, string, string) (*models.User, error)); ok { + return rf(username, email, password) + } if rf, ok := ret.Get(0).(func(string, string, string) *models.User); ok { r0 = rf(username, email, password) } else { @@ -54,7 +57,6 @@ func (_m *UserService) NewUser(username string, email string, password string) ( } } - var r1 error if rf, ok := ret.Get(1).(func(string, string, string) error); ok { r1 = rf(username, email, password) } else { diff --git a/commands/init.go b/modules.go similarity index 53% rename from commands/init.go rename to modules.go index 1d9b4f20..fa817321 100644 --- a/commands/init.go +++ b/modules.go @@ -1,11 +1,40 @@ -package commands +package badaas import ( "strings" + "go.uber.org/fx" + "go.uber.org/zap" + "github.com/ditrit/badaas/configuration" + "github.com/ditrit/badaas/controllers" + "github.com/ditrit/badaas/router" + "github.com/ditrit/badaas/router/middlewares" + "github.com/ditrit/badaas/services" "github.com/ditrit/badaas/services/userservice" - "go.uber.org/zap" +) + +var InfoModule = fx.Module( + "info", + // controller + fx.Provide(controllers.NewInfoController), + // routes + fx.Invoke(router.AddInfoRoutes), +) + +var AuthModule = fx.Module( + "auth", + // service + services.AuthServiceModule, + + // controller + fx.Provide(controllers.NewBasicAuthenticationController), + + // routes + fx.Provide(middlewares.NewAuthenticationMiddleware), + fx.Invoke(router.AddAuthRoutes), + + fx.Invoke(createSuperUser), ) // Create a super user @@ -21,7 +50,9 @@ func createSuperUser( logger.Sugar().Errorf("failed to save the super admin %w", err) return err } + logger.Sugar().Infof("The superadmin user already exists in database") } + return nil } diff --git a/commands/init_test.go b/modules_test.go similarity index 84% rename from commands/init_test.go rename to modules_test.go index 1966506d..91976ca8 100644 --- a/commands/init_test.go +++ b/modules_test.go @@ -1,26 +1,29 @@ -package commands +package badaas import ( "errors" "testing" - mocks "github.com/ditrit/badaas/mocks/configuration" - mockUserServices "github.com/ditrit/badaas/mocks/services/userservice" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" + + mocksConfiguration "github.com/ditrit/badaas/mocks/configuration" + mockUserServices "github.com/ditrit/badaas/mocks/services/userservice" ) func TestCreateSuperUser(t *testing.T) { core, _ := observer.New(zap.DebugLevel) logger := zap.New(core) - initializationConfig := mocks.NewInitializationConfiguration(t) + initializationConfig := mocksConfiguration.NewInitializationConfiguration(t) initializationConfig.On("GetAdminPassword").Return("adminpassword") + userService := mockUserServices.NewUserService(t) userService. On("NewUser", "admin", "admin-no-reply@badaas.com", "adminpassword"). Return(nil, nil) + err := createSuperUser( initializationConfig, logger, @@ -32,12 +35,14 @@ func TestCreateSuperUser(t *testing.T) { func TestCreateSuperUser_UserExists(t *testing.T) { core, logs := observer.New(zap.DebugLevel) logger := zap.New(core) - initializationConfig := mocks.NewInitializationConfiguration(t) + initializationConfig := mocksConfiguration.NewInitializationConfiguration(t) initializationConfig.On("GetAdminPassword").Return("adminpassword") + userService := mockUserServices.NewUserService(t) userService. On("NewUser", "admin", "admin-no-reply@badaas.com", "adminpassword"). Return(nil, errors.New("user already exist in database")) + err := createSuperUser( initializationConfig, logger, @@ -51,12 +56,14 @@ func TestCreateSuperUser_UserExists(t *testing.T) { func TestCreateSuperUser_UserServiceError(t *testing.T) { core, logs := observer.New(zap.DebugLevel) logger := zap.New(core) - initializationConfig := mocks.NewInitializationConfiguration(t) + initializationConfig := mocksConfiguration.NewInitializationConfiguration(t) initializationConfig.On("GetAdminPassword").Return("adminpassword") + userService := mockUserServices.NewUserService(t) userService. On("NewUser", "admin", "admin-no-reply@badaas.com", "adminpassword"). Return(nil, errors.New("email not valid")) + err := createSuperUser( initializationConfig, logger, diff --git a/orm/README.md b/orm/README.md new file mode 100644 index 00000000..5af8dec2 --- /dev/null +++ b/orm/README.md @@ -0,0 +1,23 @@ +# BaDaaS ORM: Backend and Distribution ORM (Object Relational Mapping) + +Badaas-orm is the BaDaaS' component that allows for easy and safe persistence and querying of objects but it can be used both within a BaDaaS application and independently. + +It's built on top of `gorm `_, a library that actually provides the functionality of an ORM: mapping objects to tables in the SQL database. While gorm does this job well with its automatic migration then performing queries on these objects is somewhat limited, forcing us to write SQL queries directly when they are complex. Badaas-orm seeks to address these limitations with a query system that: + +- Is compile-time safe: its query system is validated at compile time to avoid errors such as comparing attributes that are of different types, trying to use attributes or navigate relationships that do not exist, using information from tables that are not included in the query, etc. +- Is easy to use: the use of this system does not require knowledge of databases, SQL languages or complex concepts. Writing queries only requires programming in go and the result is easy to read. +- Is designed for real applications: the query system is designed to work well in real-world cases where queries are complex, require navigating multiple relationships, performing multiple comparisons, etc. +- Is designed so that developers can focus on the business model: its queries allow easy retrieval of model relationships to apply business logic to the model and it provides mechanisms to avoid errors in the business logic due to mistakes in loading information from the database. +- It is designed for high performance: the query system avoids as much as possible the use of reflection and aims that all the necessary model data can be retrieved in a single query to the database. + +## Documentation + + + +## Contributing + +See [this section](../docs/contributing/contributing.md) to view the badaas contribution guidelines. + +## License + +Badaas is Licensed under the [Mozilla Public License Version 2.0](../LICENSE). diff --git a/orm/condition/collection_preload_condition.go b/orm/condition/collection_preload_condition.go new file mode 100644 index 00000000..d108608d --- /dev/null +++ b/orm/condition/collection_preload_condition.go @@ -0,0 +1,66 @@ +package condition + +import ( + "github.com/elliotchance/pie/v2" + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" +) + +// Condition used to the preload a collection of models of a model +type collectionPreloadCondition[T1, T2 model.Model] struct { + CollectionField string + NestedPreloads []JoinCondition[T2] +} + +func (condition collectionPreloadCondition[T1, T2]) InterfaceVerificationMethod(_ T1) { + // This method is necessary to get the compiler to verify + // that an object is of type Condition[T1] +} + +func (condition collectionPreloadCondition[T1, T2]) ApplyTo(queryV *query.GormQuery, _ query.Table) error { + if len(condition.NestedPreloads) == 0 { + queryV.Preload(condition.CollectionField) + return nil + } + + queryV.Preload( + condition.CollectionField, + func(db *gorm.DB) *gorm.DB { + preloadsAsCondition := pie.Map( + condition.NestedPreloads, + func(joinCondition JoinCondition[T2]) Condition[T2] { + return joinCondition + }, + ) + + preloadQuery, err := ApplyConditions[T2](db, preloadsAsCondition) + if err != nil { + _ = db.AddError(err) + return db + } + + return preloadQuery.GormDB + }, + ) + + return nil +} + +// Condition used to the preload a collection of models of a model +func NewCollectionPreloadCondition[T1, T2 model.Model]( + collectionField string, + nestedPreloads []JoinCondition[T2], +) Condition[T1] { + if pie.Any(nestedPreloads, func(nestedPreload JoinCondition[T2]) bool { + return !nestedPreload.makesPreload() || nestedPreload.makesFilter() + }) { + return newInvalidCondition[T1](onlyPreloadsAllowedError[T1](collectionField)) + } + + return collectionPreloadCondition[T1, T2]{ + CollectionField: collectionField, + NestedPreloads: nestedPreloads, + } +} diff --git a/orm/condition/condition.go b/orm/condition/condition.go new file mode 100644 index 00000000..58e65220 --- /dev/null +++ b/orm/condition/condition.go @@ -0,0 +1,42 @@ +package condition + +import ( + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" +) + +type Condition[T model.Model] interface { + // Applies the condition to the "query" + // using the table holding + // the data for object of type T + ApplyTo(*query.GormQuery, query.Table) error + + // This method is necessary to get the compiler to verify + // that an object is of type Condition[T], + // since if no method receives by parameter a type T, + // any other Condition[T2] would also be considered a Condition[T]. + InterfaceVerificationMethod(T) +} + +// Create a GormQuery to which the conditions are applied +func ApplyConditions[T model.Model](db *gorm.DB, conditions []Condition[T]) (*query.GormQuery, error) { + model := *new(T) + + initialTable, err := query.NewTable(db, model) + if err != nil { + return nil, err + } + + query := query.NewGormQuery(db, model, initialTable) + + for _, condition := range conditions { + err := condition.ApplyTo(query, initialTable) + if err != nil { + return nil, err + } + } + + return query, nil +} diff --git a/orm/condition/connection_condition.go b/orm/condition/connection_condition.go new file mode 100644 index 00000000..f250e134 --- /dev/null +++ b/orm/condition/connection_condition.go @@ -0,0 +1,63 @@ +package condition + +import ( + "strings" + + "github.com/elliotchance/pie/v2" + + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/orm/sql" +) + +// Condition that connects multiple conditions. +// Example: condition1 AND condition2 +type connectionCondition[T model.Model] struct { + Connector sql.Operator + Conditions []WhereCondition[T] +} + +func (condition connectionCondition[T]) InterfaceVerificationMethod(_ T) { + // This method is necessary to get the compiler to verify + // that an object is of type Condition[T] +} + +func (condition connectionCondition[T]) ApplyTo(query *query.GormQuery, table query.Table) error { + return ApplyWhereCondition[T](condition, query, table) +} + +func (condition connectionCondition[T]) GetSQL(query *query.GormQuery, table query.Table) (string, []any, error) { + sqlStrings := []string{} + values := []any{} + + for _, internalCondition := range condition.Conditions { + internalSQLString, internalValues, err := internalCondition.GetSQL(query, table) + if err != nil { + return "", nil, err + } + + sqlStrings = append(sqlStrings, internalSQLString) + + values = append(values, internalValues...) + } + + return strings.Join( + sqlStrings, + " "+condition.Connector.String()+" ", + ), values, nil +} + +func (condition connectionCondition[T]) AffectsDeletedAt() bool { + return pie.Any(condition.Conditions, func(internalCondition WhereCondition[T]) bool { + return internalCondition.AffectsDeletedAt() + }) +} + +// Condition that connects multiple conditions. +// Example: condition1 AND condition2 +func NewConnectionCondition[T model.Model](connector sql.Operator, conditions ...WhereCondition[T]) WhereCondition[T] { + return connectionCondition[T]{ + Connector: connector, + Conditions: conditions, + } +} diff --git a/orm/condition/container_condition.go b/orm/condition/container_condition.go new file mode 100644 index 00000000..305a62c6 --- /dev/null +++ b/orm/condition/container_condition.go @@ -0,0 +1,51 @@ +package condition + +import ( + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/orm/sql" +) + +// Condition that contains a internal condition. +// Example: NOT (internal condition) +type containerCondition[T model.Model] struct { + ConnectionCondition WhereCondition[T] + Prefix sql.Operator +} + +func (condition containerCondition[T]) InterfaceVerificationMethod(_ T) { + // This method is necessary to get the compiler to verify + // that an object is of type Condition[T] +} + +func (condition containerCondition[T]) ApplyTo(query *query.GormQuery, table query.Table) error { + return ApplyWhereCondition[T](condition, query, table) +} + +func (condition containerCondition[T]) GetSQL(query *query.GormQuery, table query.Table) (string, []any, error) { + sqlString, values, err := condition.ConnectionCondition.GetSQL(query, table) + if err != nil { + return "", nil, err + } + + sqlString = condition.Prefix.String() + " (" + sqlString + ")" + + return sqlString, values, nil +} + +func (condition containerCondition[T]) AffectsDeletedAt() bool { + return condition.ConnectionCondition.AffectsDeletedAt() +} + +// Condition that contains a internal condition. +// Example: NOT (internal condition) +func NewContainerCondition[T model.Model](prefix sql.Operator, conditions ...WhereCondition[T]) WhereCondition[T] { + if len(conditions) == 0 { + return newInvalidCondition[T](emptyConditionsError[T](prefix)) + } + + return containerCondition[T]{ + Prefix: prefix, + ConnectionCondition: And(conditions...), + } +} diff --git a/orm/condition/dynamic_condition.go b/orm/condition/dynamic_condition.go new file mode 100644 index 00000000..92b67253 --- /dev/null +++ b/orm/condition/dynamic_condition.go @@ -0,0 +1,13 @@ +package condition + +import "github.com/ditrit/badaas/orm/model" + +type DynamicCondition[T model.Model] interface { + WhereCondition[T] + + // Allows to choose which number of join use + // for the operation in position "operationNumber" + // when the value is a field and its model is joined more than once. + // Does nothing if the operationNumber is bigger than the amount of operations. + SelectJoin(operationNumber, joinNumber uint) DynamicCondition[T] +} diff --git a/orm/condition/errors.go b/orm/condition/errors.go new file mode 100644 index 00000000..aafcf390 --- /dev/null +++ b/orm/condition/errors.go @@ -0,0 +1,36 @@ +package condition + +import ( + "fmt" + + "github.com/ditrit/badaas/orm/errors" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/sql" +) + +func conditionOperatorError[TObject model.Model, TAtribute any](operatorErr error, condition fieldCondition[TObject, TAtribute]) error { + return fmt.Errorf( + "%w; model: %T, field: %s", + operatorErr, + *new(TObject), + condition.FieldIdentifier.Field, + ) +} + +func emptyConditionsError[T model.Model](connector sql.Operator) error { + return fmt.Errorf( + "%w; connector: %s; model: %T", + errors.ErrEmptyConditions, + connector.Name(), + *new(T), + ) +} + +func onlyPreloadsAllowedError[T model.Model](fieldName string) error { + return fmt.Errorf( + "%w; model: %T, field: %s", + errors.ErrOnlyPreloadsAllowed, + *new(T), + fieldName, + ) +} diff --git a/orm/condition/field_condition.go b/orm/condition/field_condition.go new file mode 100644 index 00000000..a006b180 --- /dev/null +++ b/orm/condition/field_condition.go @@ -0,0 +1,62 @@ +package condition + +import ( + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/operator" + "github.com/ditrit/badaas/orm/query" +) + +const deletedAtField = "DeletedAt" + +// Condition that verifies the value of a field, +// using the Operator +type fieldCondition[TObject model.Model, TAtribute any] struct { + FieldIdentifier query.FieldIdentifier[TAtribute] + Operator operator.Operator[TAtribute] +} + +func (condition fieldCondition[TObject, TAtribute]) InterfaceVerificationMethod(_ TObject) { + // This method is necessary to get the compiler to verify + // that an object is of type Condition[T] +} + +// Returns a gorm Where condition that can be used +// to filter that the Field as a value of Value +func (condition fieldCondition[TObject, TAtribute]) ApplyTo(query *query.GormQuery, table query.Table) error { + return ApplyWhereCondition[TObject](condition, query, table) +} + +func (condition fieldCondition[TObject, TAtribute]) AffectsDeletedAt() bool { + return condition.FieldIdentifier.Field == deletedAtField +} + +func (condition fieldCondition[TObject, TAtribute]) GetSQL(query *query.GormQuery, table query.Table) (string, []any, error) { + sqlString, values, err := condition.Operator.ToSQL( + query, + condition.FieldIdentifier.ColumnSQL(query, table), + ) + if err != nil { + return "", nil, conditionOperatorError[TObject](err, condition) + } + + return sqlString, values, nil +} + +func (condition *fieldCondition[TObject, TAtribute]) SelectJoin(operationNumber, joinNumber uint) DynamicCondition[TObject] { + dynamicOperator, isDynamic := condition.Operator.(operator.DynamicOperator[TAtribute]) + if isDynamic { + condition.Operator = dynamicOperator.SelectJoin(operationNumber, joinNumber) + } + + return condition +} + +func NewFieldCondition[TObject model.Model, TAtribute any]( + fieldIdentifier query.FieldIdentifier[TAtribute], + operator operator.Operator[TAtribute], +) DynamicCondition[TObject] { + return &fieldCondition[TObject, TAtribute]{ + FieldIdentifier: fieldIdentifier, + Operator: operator, + } +} diff --git a/orm/condition/invalid_condition.go b/orm/condition/invalid_condition.go new file mode 100644 index 00000000..004c9f69 --- /dev/null +++ b/orm/condition/invalid_condition.go @@ -0,0 +1,32 @@ +package condition + +import "github.com/ditrit/badaas/orm/query" + +// Condition used to returns an error when the query is executed +type invalidCondition[T any] struct { + Err error +} + +func (condition invalidCondition[T]) InterfaceVerificationMethod(_ T) { + // This method is necessary to get the compiler to verify + // that an object is of type Condition[T] +} + +func (condition invalidCondition[T]) ApplyTo(_ *query.GormQuery, _ query.Table) error { + return condition.Err +} + +func (condition invalidCondition[T]) GetSQL(_ *query.GormQuery, _ query.Table) (string, []any, error) { + return "", nil, condition.Err +} + +func (condition invalidCondition[T]) AffectsDeletedAt() bool { + return false +} + +// Condition used to returns an error when the query is executed +func newInvalidCondition[T any](err error) invalidCondition[T] { + return invalidCondition[T]{ + Err: err, + } +} diff --git a/orm/condition/join_condition.go b/orm/condition/join_condition.go new file mode 100644 index 00000000..897fcf4b --- /dev/null +++ b/orm/condition/join_condition.go @@ -0,0 +1,206 @@ +package condition + +import ( + "fmt" + + "github.com/elliotchance/pie/v2" + + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" +) + +// Condition that joins T with any other model +type JoinCondition[T model.Model] interface { + Condition[T] + + // Returns true if this condition or any nested condition makes a preload + makesPreload() bool + + // Returns true if the condition of nay nested condition applies a filter (has where conditions) + makesFilter() bool +} + +// Condition that joins T with any other model +func NewJoinCondition[T1, T2 model.Model]( + conditions []Condition[T2], + relationField string, + t1Field string, + t1PreloadCondition Condition[T1], + t2Field string, +) JoinCondition[T1] { + return joinConditionImpl[T1, T2]{ + Conditions: conditions, + RelationField: relationField, + T1Field: t1Field, + T1PreloadCondition: t1PreloadCondition, + T2Field: t2Field, + } +} + +// Implementation of join condition +type joinConditionImpl[T1, T2 model.Model] struct { + T1Field string + T2Field string + RelationField string + Conditions []Condition[T2] + // condition to preload T1 in case T2 any nested object is preloaded by user + T1PreloadCondition Condition[T1] +} + +func (condition joinConditionImpl[T1, T2]) InterfaceVerificationMethod(_ T1) { + // This method is necessary to get the compiler to verify + // that an object is of type Condition[T] +} + +// Returns true if this condition or any nested condition makes a preload +func (condition joinConditionImpl[T1, T2]) makesPreload() bool { + _, joinConditions, t2PreloadCondition := divideConditionsByType(condition.Conditions) + + return t2PreloadCondition != nil || pie.Any(joinConditions, func(cond JoinCondition[T2]) bool { + return cond.makesPreload() + }) +} + +// Returns true if the condition of nay nested condition applies a filter (has where conditions) +// +//nolint:unused // is used +func (condition joinConditionImpl[T1, T2]) makesFilter() bool { + whereConditions, joinConditions, _ := divideConditionsByType(condition.Conditions) + + return len(whereConditions) != 0 || pie.Any(joinConditions, func(cond JoinCondition[T2]) bool { + return cond.makesFilter() + }) +} + +// Applies a join between the tables of T1 and T2 +// previousTableName is the name of the table of T1 +// It also applies the nested conditions +func (condition joinConditionImpl[T1, T2]) ApplyTo(query *query.GormQuery, t1Table query.Table) error { + whereConditions, joinConditions, t2PreloadCondition := divideConditionsByType(condition.Conditions) + + t2Model := *new(T2) + + // get the sql to do the join with T2 + t2Table, err := t1Table.DeliverTable(query, t2Model, condition.RelationField) + if err != nil { + return err + } + + makesPreload := condition.makesPreload() + joinQuery := condition.getSQLJoin( + query, + t1Table, + t2Table, + len(whereConditions) == 0 && makesPreload, + ) + + query.AddConcernedModel( + t2Model, + t2Table, + ) + + // apply WhereConditions to the join in the "on" clause + connectionCondition := And(whereConditions...) + + onQuery, onValues, err := connectionCondition.GetSQL(query, t2Table) + if err != nil { + return err + } + + if onQuery != "" { + joinQuery += " AND " + onQuery + } + + if !connectionCondition.AffectsDeletedAt() { + joinQuery += fmt.Sprintf( + " AND %s.deleted_at IS NULL", + t2Table.Alias, + ) + } + + // add the join to the query + query.Joins(joinQuery, onValues...) + + // apply T1 preload condition + // if this condition has a T2 preload condition + // or any nested join condition has a preload condition + // and this is not first level (T1 is the type of the repository) + // because T1 is always loaded in that case + if makesPreload && !t1Table.IsInitial() { + err = condition.T1PreloadCondition.ApplyTo(query, t1Table) + if err != nil { + return err + } + } + + // apply T2 preload condition + if t2PreloadCondition != nil { + err = t2PreloadCondition.ApplyTo(query, t2Table) + if err != nil { + return err + } + } + + // apply nested joins + for _, joinCondition := range joinConditions { + err = joinCondition.ApplyTo(query, t2Table) + if err != nil { + return err + } + } + + return nil +} + +// Returns the SQL string to do a join between T1 and T2 +// taking into account that the ID attribute necessary to do it +// can be either in T1's or T2's table. +func (condition joinConditionImpl[T1, T2]) getSQLJoin( + query *query.GormQuery, + t1Table query.Table, + t2Table query.Table, + isLeftJoin bool, +) string { + joinString := "INNER JOIN" + if isLeftJoin { + joinString = "LEFT JOIN" + } + + return fmt.Sprintf( + `%[6]s %[1]s %[2]s ON %[2]s.%[3]s = %[4]s.%[5]s + `, + t2Table.Name, + t2Table.Alias, + query.ColumnName(t2Table, condition.T2Field), + t1Table.Alias, + query.ColumnName(t1Table, condition.T1Field), + joinString, + ) +} + +// Divides a list of conditions by its type: WhereConditions and JoinConditions +func divideConditionsByType[T model.Model]( + conditions []Condition[T], +) (whereConditions []WhereCondition[T], joinConditions []JoinCondition[T], preload *preloadCondition[T]) { + for _, condition := range conditions { + possibleWhereCondition, ok := condition.(WhereCondition[T]) + if ok { + whereConditions = append(whereConditions, possibleWhereCondition) + continue + } + + possiblePreloadCondition, ok := condition.(preloadCondition[T]) + if ok { + preload = &possiblePreloadCondition + continue + } + + possibleJoinCondition, ok := condition.(JoinCondition[T]) + if ok { + joinConditions = append(joinConditions, possibleJoinCondition) + continue + } + } + + return +} diff --git a/orm/condition/logical.go b/orm/condition/logical.go new file mode 100644 index 00000000..e5edb34b --- /dev/null +++ b/orm/condition/logical.go @@ -0,0 +1,10 @@ +package condition + +import ( + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/sql" +) + +func And[T model.Model](conditions ...WhereCondition[T]) WhereCondition[T] { + return NewConnectionCondition(sql.And, conditions...) +} diff --git a/orm/condition/preload_condition.go b/orm/condition/preload_condition.go new file mode 100644 index 00000000..4c76bc75 --- /dev/null +++ b/orm/condition/preload_condition.go @@ -0,0 +1,31 @@ +package condition + +import ( + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" +) + +// Condition used to the preload the attributes of a model +type preloadCondition[T model.Model] struct { + Fields []query.IFieldIdentifier +} + +func (condition preloadCondition[T]) InterfaceVerificationMethod(_ T) { + // This method is necessary to get the compiler to verify + // that an object is of type Condition[T] +} + +func (condition preloadCondition[T]) ApplyTo(query *query.GormQuery, table query.Table) error { + for _, fieldID := range condition.Fields { + query.AddSelect(table, fieldID) + } + + return nil +} + +// Condition used to the preload the attributes of a model +func NewPreloadCondition[T model.Model](fields ...query.IFieldIdentifier) Condition[T] { + return preloadCondition[T]{ + Fields: fields, + } +} diff --git a/orm/condition/where_condition.go b/orm/condition/where_condition.go new file mode 100644 index 00000000..c2b540af --- /dev/null +++ b/orm/condition/where_condition.go @@ -0,0 +1,38 @@ +package condition + +import ( + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" +) + +// Conditions that can be used in a where clause +// (or in a on of a join) +type WhereCondition[T model.Model] interface { + Condition[T] + + // Get the sql string and values to use in the query + GetSQL(query *query.GormQuery, table query.Table) (string, []any, error) + + // Returns true if the DeletedAt column if affected by the condition + // If no condition affects the DeletedAt, the verification that it's null will be added automatically + AffectsDeletedAt() bool +} + +// apply WhereCondition of any type on the query +func ApplyWhereCondition[T model.Model](condition WhereCondition[T], query *query.GormQuery, table query.Table) error { + sql, values, err := condition.GetSQL(query, table) + if err != nil { + return err + } + + if condition.AffectsDeletedAt() { + query.Unscoped() + } + + query.Where( + sql, + values..., + ) + + return nil +} diff --git a/orm/db.go b/orm/db.go new file mode 100644 index 00000000..17c1fbfc --- /dev/null +++ b/orm/db.go @@ -0,0 +1,27 @@ +package orm + +import ( + "github.com/elliotchance/pie/v2" + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm/logger" +) + +// Open initialize db session based on dialector +func Open(dialector gorm.Dialector, opts ...gorm.Option) (*gorm.DB, error) { + configs := pie.Filter(opts, func(opt gorm.Option) bool { + _, isConfig := opt.(*gorm.Config) + return isConfig + }) + + if len(configs) == 0 { + return gorm.Open(dialector, append(opts, &gorm.Config{Logger: logger.Default})...) + } + + lastConfig, _ := configs[len(configs)-1].(*gorm.Config) + if lastConfig.Logger == nil { + lastConfig.Logger = logger.Default + } + + return gorm.Open(dialector, opts...) +} diff --git a/orm/dsn.go b/orm/dsn.go new file mode 100644 index 00000000..e1d7d7f9 --- /dev/null +++ b/orm/dsn.go @@ -0,0 +1,35 @@ +package orm + +import ( + "fmt" + "net" + "strconv" +) + +func CreatePostgreSQLDSN(host, username, password, sslmode, dbname string, port int) string { + return fmt.Sprintf( + "user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", + username, password, host, port, sslmode, dbname, + ) +} + +func CreateMySQLDSN(host, username, password, dbname string, port int) string { + return fmt.Sprintf( + "%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + username, password, net.JoinHostPort(host, strconv.Itoa(port)), dbname, + ) +} + +func CreateSQLiteDSN(path string) string { + return fmt.Sprintf("sqlite:%s", path) +} + +func CreateSQLServerDSN(host, username, password, dbname string, port int) string { + return fmt.Sprintf( + "sqlserver://%s:%s@%s?database=%s", + username, + password, + net.JoinHostPort(host, strconv.Itoa(port)), + dbname, + ) +} diff --git a/orm/dsn_test.go b/orm/dsn_test.go new file mode 100644 index 00000000..4bce67b0 --- /dev/null +++ b/orm/dsn_test.go @@ -0,0 +1,60 @@ +package orm + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreatePostgreDSN(t *testing.T) { + assert.Equal( + t, + "user=username password=password host=192.168.2.5 port=1225 sslmode=disable dbname=badaas_db", + CreatePostgreSQLDSN( + "192.168.2.5", + "username", + "password", + "disable", + "badaas_db", + 1225, + ), + ) +} + +func TestCreateMySQLDSN(t *testing.T) { + assert.Equal( + t, + "username:password@tcp(192.168.2.5:1225)/badaas_db?charset=utf8mb4&parseTime=True&loc=Local", + CreateMySQLDSN( + "192.168.2.5", + "username", + "password", + "badaas_db", + 1225, + ), + ) +} + +func TestCreateSQLiteDSN(t *testing.T) { + assert.Equal( + t, + "sqlite:/dir/file", + CreateSQLiteDSN( + "/dir/file", + ), + ) +} + +func TestCreateSQLServerDSN(t *testing.T) { + assert.Equal( + t, + "sqlserver://username:password@192.168.2.5:1225?database=badaas_db", + CreateSQLServerDSN( + "192.168.2.5", + "username", + "password", + "badaas_db", + 1225, + ), + ) +} diff --git a/orm/dynamic/operator.go b/orm/dynamic/operator.go new file mode 100644 index 00000000..35468b81 --- /dev/null +++ b/orm/dynamic/operator.go @@ -0,0 +1,14 @@ +package dynamic + +import ( + "github.com/ditrit/badaas/orm/operator" + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/orm/sql" +) + +func newValueOperator[T any]( + sqlOperator sql.Operator, + field query.FieldIdentifier[T], +) *operator.ValueOperator[T] { + return operator.NewValueOperator[T](sqlOperator, field) +} diff --git a/orm/dynamic/operators.go b/orm/dynamic/operators.go new file mode 100644 index 00000000..4f5320e4 --- /dev/null +++ b/orm/dynamic/operators.go @@ -0,0 +1,76 @@ +package dynamic + +import ( + "github.com/ditrit/badaas/orm/operator" + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/orm/sql" +) + +// Comparison Operators +// refs: +// - MySQL: https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html +// - PostgreSQL: https://www.postgresql.org/docs/current/functions-comparison.html +// - SQLServer: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/comparison-operators-transact-sql?view=sql-server-ver16 +// - SQLite: https://www.sqlite.org/lang_expr.html + +// EqualTo +func Eq[T any](field query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newValueOperator(sql.Eq, field) +} + +// NotEqualTo +func NotEq[T any](field query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newValueOperator(sql.NotEq, field) +} + +// LessThan +func Lt[T any](field query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newValueOperator(sql.Lt, field) +} + +// LessThanOrEqualTo +func LtOrEq[T any](field query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newValueOperator(sql.LtOrEq, field) +} + +// GreaterThan +func Gt[T any](field query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newValueOperator(sql.Gt, field) +} + +// GreaterThanOrEqualTo +func GtOrEq[T any](field query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newValueOperator(sql.GtOrEq, field) +} + +// Comparison Predicates +// refs: +// - MySQL: https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html +// - PostgreSQL: https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE +// - SQLServer: https://learn.microsoft.com/en-us/sql/t-sql/queries/predicates?view=sql-server-ver16 +// - SQLite: https://www.sqlite.org/lang_expr.html + +// Equivalent to field1 < value < field2 +func Between[T any](field1, field2 query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newBetweenOperator(sql.Between, field1, field2) +} + +// Equivalent to NOT (field1 < value < field2) +func NotBetween[T any](field1, field2 query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newBetweenOperator(sql.NotBetween, field1, field2) +} + +func newBetweenOperator[T any](sqlOperator sql.Operator, field1, field2 query.FieldIdentifier[T]) operator.DynamicOperator[T] { + operator := newValueOperator(sqlOperator, field1) + return operator.AddOperation(sql.And, field2) +} + +// Not supported by: mysql +func IsDistinct[T any](field query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newValueOperator(sql.IsDistinct, field) +} + +// Not supported by: mysql +func IsNotDistinct[T any](field query.FieldIdentifier[T]) operator.DynamicOperator[T] { + return newValueOperator(sql.IsNotDistinct, field) +} diff --git a/orm/errors.go b/orm/errors.go new file mode 100644 index 00000000..d07f9007 --- /dev/null +++ b/orm/errors.go @@ -0,0 +1,9 @@ +package orm + +import ( + "fmt" +) + +func methodError(err error, method string) error { + return fmt.Errorf("%w; method: %s", err, method) +} diff --git a/orm/errors/errors.go b/orm/errors/errors.go new file mode 100644 index 00000000..6b2b1e39 --- /dev/null +++ b/orm/errors/errors.go @@ -0,0 +1,22 @@ +package errors + +import ( + "errors" +) + +var ( + // query + ErrFieldModelNotConcerned = errors.New("field's model is not concerned by the query (not joined)") + ErrJoinMustBeSelected = errors.New("field's model is joined more than once, select which one you want to use") + + // conditions + ErrEmptyConditions = errors.New("condition must have at least one inner condition") + ErrOnlyPreloadsAllowed = errors.New("only conditions that do a preload are allowed") + + // crud + ErrMoreThanOneObjectFound = errors.New("found more that one object that meet the requested conditions") + ErrObjectNotFound = errors.New("no object exists that meets the requested conditions") + + // preload + ErrRelationNotLoaded = errors.New("relation not loaded") +) diff --git a/orm/field_is.go b/orm/field_is.go new file mode 100644 index 00000000..10146d03 --- /dev/null +++ b/orm/field_is.go @@ -0,0 +1,159 @@ +package orm + +import ( + "github.com/ditrit/badaas/orm/condition" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/operator" + "github.com/ditrit/badaas/orm/query" +) + +type FieldIs[TObject model.Model, TAttribute any] struct { + FieldID query.FieldIdentifier[TAttribute] +} + +type BoolFieldIs[TObject model.Model] struct { + FieldIs[TObject, bool] +} + +type StringFieldIs[TObject model.Model] struct { + FieldIs[TObject, string] +} + +// EqualTo +// NotDistinct must be used in cases where value can be NULL +func (is FieldIs[TObject, TAttribute]) Eq(value TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.Eq(value)) +} + +// NotEqualTo +// Distinct must be used in cases where value can be NULL +func (is FieldIs[TObject, TAttribute]) NotEq(value TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.NotEq(value)) +} + +// LessThan +func (is FieldIs[TObject, TAttribute]) Lt(value TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.Lt(value)) +} + +// LessThanOrEqualTo +func (is FieldIs[TObject, TAttribute]) LtOrEq(value TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.LtOrEq(value)) +} + +// GreaterThan +func (is FieldIs[TObject, TAttribute]) Gt(value TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.Gt(value)) +} + +// GreaterThanOrEqualTo +func (is FieldIs[TObject, TAttribute]) GtOrEq(value TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.GtOrEq(value)) +} + +// Equivalent to v1 < value < v2 +func (is FieldIs[TObject, TAttribute]) Between(v1, v2 TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.Between(v1, v2)) +} + +// Equivalent to NOT (v1 < value < v2) +func (is FieldIs[TObject, TAttribute]) NotBetween(v1, v2 TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.NotBetween(v1, v2)) +} + +func (is FieldIs[TObject, TAttribute]) Null() condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.IsNull[TAttribute]()) +} + +func (is FieldIs[TObject, TAttribute]) NotNull() condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.IsNotNull[TAttribute]()) +} + +// Not supported by: sqlserver +func (is BoolFieldIs[TObject]) True() condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, bool](is.FieldID, operator.IsTrue()) +} + +// Not supported by: sqlserver +func (is BoolFieldIs[TObject]) NotTrue() condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, bool](is.FieldID, operator.IsNotTrue()) +} + +// Not supported by: sqlserver +func (is BoolFieldIs[TObject]) False() condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, bool](is.FieldID, operator.IsFalse()) +} + +// Not supported by: sqlserver +func (is BoolFieldIs[TObject]) NotFalse() condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, bool](is.FieldID, operator.IsNotFalse()) +} + +// Not supported by: sqlserver, sqlite +func (is BoolFieldIs[TObject]) Unknown() condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, bool](is.FieldID, operator.IsUnknown()) +} + +// Not supported by: sqlserver, sqlite +func (is BoolFieldIs[TObject]) NotUnknown() condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, bool](is.FieldID, operator.IsNotUnknown()) +} + +// Not supported by: mysql +func (is FieldIs[TObject, TAttribute]) Distinct(value TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.IsDistinct(value)) +} + +// Not supported by: mysql +func (is FieldIs[TObject, TAttribute]) NotDistinct(value TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.IsNotDistinct(value)) +} + +func (is FieldIs[TObject, TAttribute]) In(values ...TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.In(values)) +} + +func (is FieldIs[TObject, TAttribute]) NotIn(values ...TAttribute) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, operator.NotIn(values)) +} + +// Pattern in all databases: +// - An underscore (_) in pattern stands for (matches) any single character. +// - A percent sign (%) matches any sequence of zero or more characters. +// +// Additionally in SQLServer: +// - Square brackets ([ ]) matches any single character within the specified range ([a-f]) or set ([abcdef]). +// - [^] matches any single character not within the specified range ([^a-f]) or set ([^abcdef]). +// +// WARNINGS: +// - SQLite: LIKE is case-insensitive unless case_sensitive_like pragma (https://www.sqlite.org/pragma.html#pragma_case_sensitive_like) is true. +// - SQLServer, MySQL: the case-sensitivity depends on the collation used in compared column. +// - PostgreSQL: LIKE is always case-sensitive, if you want case-insensitive use the ILIKE operator (implemented in psql.ILike) +// +// refs: +// - mysql: https://dev.mysql.com/doc/refman/8.0/en/string-comparison-functions.html#operator_like +// - postgresql: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE +// - sqlserver: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/like-transact-sql?view=sql-server-ver16 +// - sqlite: https://www.sqlite.org/lang_expr.html#like +func (is StringFieldIs[TObject]) Like(pattern string) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, string](is.FieldID, operator.Like(pattern)) +} + +// Custom can be used to use other Operators, like database specific operators +func (is FieldIs[TObject, TAttribute]) Custom(op operator.Operator[TAttribute]) condition.WhereCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.FieldID, op) +} + +// Dynamic transforms the FieldIs in a DynamicFieldIs to use dynamic operators +func (is FieldIs[TObject, TAttribute]) Dynamic() DynamicFieldIs[TObject, TAttribute] { + return DynamicFieldIs[TObject, TAttribute]{ + fieldID: is.FieldID, + } +} + +// Unsafe transforms the FieldIs in an UnsafeFieldIs to use unsafe operators +func (is FieldIs[TObject, TAttribute]) Unsafe() UnsafeFieldIs[TObject, TAttribute] { + return UnsafeFieldIs[TObject, TAttribute]{ + fieldID: is.FieldID, + } +} diff --git a/orm/field_is_dynamic.go b/orm/field_is_dynamic.go new file mode 100644 index 00000000..2842f933 --- /dev/null +++ b/orm/field_is_dynamic.go @@ -0,0 +1,62 @@ +package orm + +import ( + "github.com/ditrit/badaas/orm/condition" + "github.com/ditrit/badaas/orm/dynamic" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" +) + +type DynamicFieldIs[TObject model.Model, TAttribute any] struct { + fieldID query.FieldIdentifier[TAttribute] +} + +// EqualTo +func (is DynamicFieldIs[TObject, TAttribute]) Eq(field query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.Eq(field)) +} + +// NotEqualTo +func (is DynamicFieldIs[TObject, TAttribute]) NotEq(field query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.NotEq(field)) +} + +// LessThan +func (is DynamicFieldIs[TObject, TAttribute]) Lt(field query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.Lt(field)) +} + +// LessThanOrEqualTo +func (is DynamicFieldIs[TObject, TAttribute]) LtOrEq(field query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.LtOrEq(field)) +} + +// GreaterThan +func (is DynamicFieldIs[TObject, TAttribute]) Gt(field query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.Gt(field)) +} + +// GreaterThanOrEqualTo +func (is DynamicFieldIs[TObject, TAttribute]) GtOrEq(field query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.GtOrEq(field)) +} + +// Equivalent to field1 < value < field2 +func (is DynamicFieldIs[TObject, TAttribute]) Between(field1, field2 query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.Between(field1, field2)) +} + +// Equivalent to NOT (field1 < value < field2) +func (is DynamicFieldIs[TObject, TAttribute]) NotBetween(field1, field2 query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.NotBetween(field1, field2)) +} + +// Not supported by: mysql +func (is DynamicFieldIs[TObject, TAttribute]) Distinct(field query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.IsDistinct(field)) +} + +// Not supported by: mysql +func (is DynamicFieldIs[TObject, TAttribute]) NotDistinct(field query.FieldIdentifier[TAttribute]) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, dynamic.IsNotDistinct(field)) +} diff --git a/orm/field_is_unsafe.go b/orm/field_is_unsafe.go new file mode 100644 index 00000000..31233d6a --- /dev/null +++ b/orm/field_is_unsafe.go @@ -0,0 +1,62 @@ +package orm + +import ( + "github.com/ditrit/badaas/orm/condition" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/orm/unsafe" +) + +type UnsafeFieldIs[TObject model.Model, TAttribute any] struct { + fieldID query.FieldIdentifier[TAttribute] +} + +// EqualTo +func (is UnsafeFieldIs[TObject, TAttribute]) Eq(value any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.Eq[TAttribute](value)) +} + +// NotEqualTo +func (is UnsafeFieldIs[TObject, TAttribute]) NotEq(value any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.NotEq[TAttribute](value)) +} + +// LessThan +func (is UnsafeFieldIs[TObject, TAttribute]) Lt(value any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.Lt[TAttribute](value)) +} + +// LessThanOrEqualTo +func (is UnsafeFieldIs[TObject, TAttribute]) LtOrEq(value any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.LtOrEq[TAttribute](value)) +} + +// GreaterThan +func (is UnsafeFieldIs[TObject, TAttribute]) Gt(value any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.Gt[TAttribute](value)) +} + +// GreaterThanOrEqualTo +func (is UnsafeFieldIs[TObject, TAttribute]) GtOrEq(value any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.GtOrEq[TAttribute](value)) +} + +// Equivalent to field1 < value < field2 +func (is UnsafeFieldIs[TObject, TAttribute]) Between(v1, v2 any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.Between[TAttribute](v1, v2)) +} + +// Equivalent to NOT (field1 < value < field2) +func (is UnsafeFieldIs[TObject, TAttribute]) NotBetween(v1, v2 any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.NotBetween[TAttribute](v1, v2)) +} + +// Not supported by: mysql +func (is UnsafeFieldIs[TObject, TAttribute]) Distinct(value any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.IsDistinct[TAttribute](value)) +} + +// Not supported by: mysql +func (is UnsafeFieldIs[TObject, TAttribute]) NotDistinct(value any) condition.DynamicCondition[TObject] { + return condition.NewFieldCondition[TObject, TAttribute](is.fieldID, unsafe.IsNotDistinct[TAttribute](value)) +} diff --git a/orm/logger/default.go b/orm/logger/default.go new file mode 100644 index 00000000..9be2548b --- /dev/null +++ b/orm/logger/default.go @@ -0,0 +1,110 @@ +package logger + +import ( + "context" + "log" + "os" + "strconv" + "time" + + gormLogger "gorm.io/gorm/logger" +) + +const ( + defaultSlowQueryThreshold = 200 * time.Millisecond + defaultSlowTransactionThreshold = 200 * time.Millisecond + DisableThreshold = 0 +) + +var ( + DefaultConfig = Config{ + LogLevel: gormLogger.Warn, + SlowQueryThreshold: defaultSlowQueryThreshold, + SlowTransactionThreshold: defaultSlowTransactionThreshold, + IgnoreRecordNotFoundError: false, + ParameterizedQueries: false, + } + Default = New(DefaultConfig) + defaultWriter = log.New(os.Stdout, "\r\n", log.LstdFlags) +) + +type defaultLogger struct { + gormLogger.Interface + Config +} + +func New(config Config) Interface { + return NewWithWriter(config, defaultWriter) +} + +func NewWithWriter(config Config, writer gormLogger.Writer) Interface { + return &defaultLogger{ + Config: config, + Interface: gormLogger.New( + writerWrapper{Writer: writer}, + config.toGormConfig(), + ), + } +} + +func (l *defaultLogger) LogMode(level gormLogger.LogLevel) gormLogger.Interface { + // method made to satisfy gormLogger.Interface + return l.ToLogMode(level) +} + +func (l *defaultLogger) ToLogMode(level gormLogger.LogLevel) Interface { + newLogger := *l + newLogger.LogLevel = level + newLogger.Interface = newLogger.Interface.LogMode(level) + + return &newLogger +} + +const nanoToMicro = 1e6 + +func (l defaultLogger) TraceTransaction(ctx context.Context, begin time.Time) { + if l.LogLevel <= gormLogger.Silent { + return + } + + elapsed := time.Since(begin) + + switch { + case l.SlowTransactionThreshold != DisableThreshold && elapsed > l.SlowTransactionThreshold && l.LogLevel >= gormLogger.Warn: + l.Interface.Warn(ctx, "transaction_slow (>= %v) [%.3fms]", l.SlowTransactionThreshold, float64(elapsed.Nanoseconds())/nanoToMicro) + case l.LogLevel >= gormLogger.Info: + l.Interface.Info(ctx, "transaction_exec [%.3fms]", float64(elapsed.Nanoseconds())/nanoToMicro) + } +} + +type writerWrapper struct { + Writer gormLogger.Writer +} + +// Info, Warn, Error or Trace + Printf +const defaultStacktraceLen = 2 + +func (w writerWrapper) Printf(msg string, args ...interface{}) { + if len(args) > 0 { + // change the file path to avoid showing badaas-orm internal files + firstArg := args[0] + + _, isString := firstArg.(string) + if isString { + file, line, caller := FindLastCaller(defaultStacktraceLen) + if caller != 0 { + w.Writer.Printf( + msg, + append( + []any{file + ":" + strconv.FormatInt(int64(line), 10)}, + args[1:]..., + )..., + ) + + return + } + } + } + + w.Writer.Printf(msg, args...) +} diff --git a/orm/logger/default_test.go b/orm/logger/default_test.go new file mode 100644 index 00000000..1e1aaf63 --- /dev/null +++ b/orm/logger/default_test.go @@ -0,0 +1,92 @@ +package logger_test + +import ( + "bytes" + "context" + "errors" + "log" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/ditrit/badaas/orm/logger" +) + +func TestTraceError(t *testing.T) { + var buffer bytes.Buffer + + logger := logger.NewWithWriter(logger.DefaultConfig, log.New(&buffer, "\r\n", log.LstdFlags)) + err := errors.New("sql error") + logger.Trace( + context.Background(), + time.Now(), + func() (string, int64) { + return "fail sql", -1 + }, + err, + ) + + assert.Contains(t, buffer.String(), "sql error") + assert.Contains(t, buffer.String(), "[rows:-]") + assert.Contains(t, buffer.String(), "fail sql") +} + +func TestTraceSlowQuery(t *testing.T) { + var buffer bytes.Buffer + + logger := logger.NewWithWriter(logger.DefaultConfig, log.New(&buffer, "\r\n", log.LstdFlags)) + logger.Trace( + context.Background(), + time.Now().Add(-300*time.Millisecond), + func() (string, int64) { + return "slow sql", 1 + }, + nil, + ) + + assert.Contains(t, buffer.String(), "SLOW SQL >= 200ms") + assert.Contains(t, buffer.String(), "[rows:1]") + assert.Contains(t, buffer.String(), "slow sql") +} + +func TestTraceQueryExec(t *testing.T) { + var buffer bytes.Buffer + + logger := logger.NewWithWriter(logger.DefaultConfig, log.New(&buffer, "\r\n", log.LstdFlags)).ToLogMode(logger.Info) + logger.Trace( + context.Background(), + time.Now().Add(3*time.Hour), + func() (string, int64) { + return "normal sql", 1 + }, + nil, + ) + + assert.Contains(t, buffer.String(), "[rows:1]") + assert.Contains(t, buffer.String(), "normal sql") +} + +func TestTraceSlowTransaction(t *testing.T) { + var buffer bytes.Buffer + + logger := logger.NewWithWriter(logger.DefaultConfig, log.New(&buffer, "\r\n", log.LstdFlags)) + logger.TraceTransaction( + context.Background(), + time.Now().Add(-300*time.Millisecond), + ) + + assert.Contains(t, buffer.String(), "transaction_slow (>= 200ms)") +} + +func TestTraceTransactionExec(t *testing.T) { + var buffer bytes.Buffer + + logger := logger.NewWithWriter(logger.DefaultConfig, log.New(&buffer, "\r\n", log.LstdFlags)).ToLogMode(logger.Info) + logger.TraceTransaction( + context.Background(), + time.Now().Add(3*time.Hour), + ) + + assert.Contains(t, buffer.String(), "transaction_exec") +} diff --git a/orm/logger/gormzap/gormzap.go b/orm/logger/gormzap/gormzap.go new file mode 100644 index 00000000..2936e840 --- /dev/null +++ b/orm/logger/gormzap/gormzap.go @@ -0,0 +1,170 @@ +package gormzap + +import ( + "context" + "errors" + "fmt" + "strconv" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gorm.io/gorm" + gormLogger "gorm.io/gorm/logger" + + "github.com/ditrit/badaas/orm/logger" +) + +// This type implement the [logger.Interface] interface. +// It is to be used as a replacement for the original logger +type gormzap struct { + logger.Config + ZapLogger *zap.Logger +} + +// The constructor of the gormzap logger with default config +func NewDefault(zapLogger *zap.Logger) logger.Interface { + return New(zapLogger, logger.DefaultConfig) +} + +// The constructor of the gormzap logger +func New(zapLogger *zap.Logger, config logger.Config) logger.Interface { + return &gormzap{ + ZapLogger: zapLogger, + Config: config, + } +} + +// Set the GORM's log mode to the value passed as argument +// Take into account that zap logger also have a log level +// that will determine if this logs are written or not +// GORM Info logs will generate a log with DebugLevel +// GORM Warn logs will generate a log with WarnLevel +// GORM Error logs will generate a log with ErrorLevel +func (l *gormzap) LogMode(level gormLogger.LogLevel) gormLogger.Interface { + // method made to satisfy gormLogger.Interface + return l.ToLogMode(level) +} + +// Set the GORM's log mode to the value passed as argument +// Take into account that zap logger also have a log level +// that will determine if this logs are written or not +// GORM Info logs will generate a log with DebugLevel +// GORM Warn logs will generate a log with WarnLevel +// GORM Error logs will generate a log with ErrorLevel +func (l *gormzap) ToLogMode(level gormLogger.LogLevel) logger.Interface { + newLogger := *l + newLogger.LogLevel = level + + return &newLogger +} + +// log info +func (l gormzap) Info(_ context.Context, str string, args ...interface{}) { + if l.LogLevel >= gormLogger.Info { + l.logger().Sugar().Debugf(str, args...) + } +} + +// log warning +func (l gormzap) Warn(_ context.Context, str string, args ...interface{}) { + if l.LogLevel >= gormLogger.Warn { + l.logger().Sugar().Warnf(str, args...) + } +} + +// log an error +func (l gormzap) Error(_ context.Context, str string, args ...interface{}) { + if l.LogLevel >= gormLogger.Error { + l.logger().Sugar().Errorf(str, args...) + } +} + +// log a trace +func (l gormzap) Trace( + _ context.Context, + begin time.Time, + fc func() (sql string, rowsAffected int64), + err error, +) { + if l.LogLevel <= gormLogger.Silent { + return + } + + elapsedTime := time.Since(begin) + + switch { + case err != nil && l.LogLevel >= gormLogger.Error && (!l.IgnoreRecordNotFoundError || !errors.Is(err, gorm.ErrRecordNotFound)): + sql, rowsAffected := fc() + l.logger().Error( + "query_error", + append(getZapFields(elapsedTime, rowsAffected, sql), zap.Error(err))..., + ) + case l.SlowQueryThreshold != logger.DisableThreshold && elapsedTime > l.SlowQueryThreshold && l.LogLevel >= gormLogger.Warn: + sql, rowsAffected := fc() + l.logger().Warn( + fmt.Sprintf("query_slow (>= %v)", l.SlowQueryThreshold), + getZapFields(elapsedTime, rowsAffected, sql)..., + ) + case l.LogLevel >= gormLogger.Info: + sql, rowsAffected := fc() + l.logger().Debug( + "query_exec", + getZapFields(elapsedTime, rowsAffected, sql)..., + ) + } +} + +func getZapFields(elapsedTime time.Duration, rowsAffected int64, sql string) []zapcore.Field { + rowsAffectedString := strconv.FormatInt(rowsAffected, 10) + if rowsAffected == -1 { + rowsAffectedString = "-" + } + + return []zapcore.Field{ + zap.Duration("elapsed_time", elapsedTime), + zap.String("rows_affected", rowsAffectedString), + zap.String("sql", sql), + } +} + +func (l gormzap) TraceTransaction(_ context.Context, begin time.Time) { + elapsed := time.Since(begin) + + switch { + case l.SlowTransactionThreshold != logger.DisableThreshold && elapsed > l.SlowTransactionThreshold && l.LogLevel >= gormLogger.Warn: + l.logger().Warn( + fmt.Sprintf("transaction_slow (>= %v)", l.SlowTransactionThreshold), + zap.Duration("elapsed_time", elapsed), + ) + case l.LogLevel >= gormLogger.Info: + l.logger().Debug( + "transaction_exec", + zap.Duration("elapsed_time", elapsed), + ) + } +} + +// Filter parameters from queries depending of the value of ParameterizedQueries +func (l gormzap) ParamsFilter(_ context.Context, sql string, params ...interface{}) (string, []interface{}) { + if l.ParameterizedQueries { + return sql, nil + } + + return sql, params +} + +// Info, Warn, Error or Trace + logger +const gormzapStacktraceLen = 2 + +// return a logger that log the right caller +func (l gormzap) logger() *zap.Logger { + _, _, caller := logger.FindLastCaller(gormzapStacktraceLen) + if caller == 0 { + // in case we checked in all the stacktrace and none meet the conditions, + // return the zap logger with the caller of gormzap, no matter where + return l.ZapLogger.WithOptions(zap.AddCallerSkip(gormzapStacktraceLen)) + } + + return l.ZapLogger.WithOptions(zap.AddCallerSkip(caller - 1)) // -1 because here is how many we want to skip +} diff --git a/orm/logger/gormzap/gormzap_test.go b/orm/logger/gormzap/gormzap_test.go new file mode 100644 index 00000000..bf73e76a --- /dev/null +++ b/orm/logger/gormzap/gormzap_test.go @@ -0,0 +1,122 @@ +package gormzap_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" + + "github.com/ditrit/badaas/orm/logger" + "github.com/ditrit/badaas/orm/logger/gormzap" +) + +func TestTraceError(t *testing.T) { + core, logs := observer.New(zap.DebugLevel) + zapLogger := zap.New(core) + + logger := gormzap.NewDefault(zapLogger) + err := errors.New("sql error") + logger.Trace( + context.Background(), + time.Now(), + func() (string, int64) { + return "fail sql", -1 + }, + err, + ) + + require.Equal(t, 1, logs.Len()) + log := logs.All()[0] + assert.Equal(t, log.Level, zapcore.ErrorLevel) + assert.Equal(t, log.Message, "query_error") + require.Len(t, log.Context, 4) + assert.Contains(t, log.Context, zap.Field{Key: "error", Type: zapcore.ErrorType, Interface: err}) + assert.Contains(t, log.Context, zap.Field{Key: "rows_affected", Type: zapcore.StringType, String: "-"}) + assert.Contains(t, log.Context, zap.Field{Key: "sql", Type: zapcore.StringType, String: "fail sql"}) +} + +func TestTraceSlowQuery(t *testing.T) { + core, logs := observer.New(zap.DebugLevel) + zapLogger := zap.New(core) + + logger := gormzap.NewDefault(zapLogger) + logger.Trace( + context.Background(), + time.Now().Add(-300*time.Millisecond), + func() (string, int64) { + return "slow sql", 1 + }, + nil, + ) + + require.Equal(t, 1, logs.Len()) + log := logs.All()[0] + assert.Equal(t, log.Level, zapcore.WarnLevel) + assert.Equal(t, log.Message, "query_slow (>= 200ms)") + require.Len(t, log.Context, 3) + assert.Contains(t, log.Context, zap.Field{Key: "rows_affected", Type: zapcore.StringType, String: "1"}) + assert.Contains(t, log.Context, zap.Field{Key: "sql", Type: zapcore.StringType, String: "slow sql"}) +} + +func TestTraceQueryExec(t *testing.T) { + core, logs := observer.New(zap.DebugLevel) + zapLogger := zap.New(core) + + logger := gormzap.NewDefault(zapLogger).ToLogMode(logger.Info) + logger.Trace( + context.Background(), + time.Now().Add(3*time.Hour), + func() (string, int64) { + return "normal sql", 1 + }, + nil, + ) + + require.Equal(t, 1, logs.Len()) + log := logs.All()[0] + assert.Equal(t, log.Level, zapcore.DebugLevel) + assert.Equal(t, log.Message, "query_exec") + require.Len(t, log.Context, 3) + assert.Contains(t, log.Context, zap.Field{Key: "rows_affected", Type: zapcore.StringType, String: "1"}) + assert.Contains(t, log.Context, zap.Field{Key: "sql", Type: zapcore.StringType, String: "normal sql"}) +} + +func TestTraceSlowTransaction(t *testing.T) { + core, logs := observer.New(zap.DebugLevel) + zapLogger := zap.New(core) + + logger := gormzap.NewDefault(zapLogger) + logger.TraceTransaction( + context.Background(), + time.Now().Add(-300*time.Millisecond), + ) + + require.Equal(t, 1, logs.Len()) + log := logs.All()[0] + assert.Equal(t, log.Level, zapcore.WarnLevel) + assert.Equal(t, log.Message, "transaction_slow (>= 200ms)") + require.Len(t, log.Context, 1) +} + +func TestTraceTransactionExec(t *testing.T) { + core, logs := observer.New(zap.DebugLevel) + zapLogger := zap.New(core) + + logger := gormzap.NewDefault(zapLogger).ToLogMode(logger.Info) + logger.TraceTransaction( + context.Background(), + time.Now().Add(3*time.Hour), + ) + + require.Equal(t, 1, logs.Len()) + log := logs.All()[0] + assert.Equal(t, log.Level, zapcore.DebugLevel) + assert.Equal(t, log.Message, "transaction_exec") + require.Len(t, log.Context, 1) +} diff --git a/orm/logger/logger.go b/orm/logger/logger.go new file mode 100644 index 00000000..281a7929 --- /dev/null +++ b/orm/logger/logger.go @@ -0,0 +1,86 @@ +package logger + +import ( + "context" + "path/filepath" + "runtime" + "strings" + "time" + + gormLogger "gorm.io/gorm/logger" +) + +type Interface interface { + gormLogger.Interface + // change log mode + ToLogMode(gormLogger.LogLevel) Interface + // Trace a committed transaction + TraceTransaction(ctx context.Context, begin time.Time) +} + +const ( + // Silent silent log level + Silent gormLogger.LogLevel = gormLogger.Silent + // Error error log level + Error gormLogger.LogLevel = gormLogger.Error + // Warn warn log level + Warn gormLogger.LogLevel = gormLogger.Warn + // Info info log level + Info gormLogger.LogLevel = gormLogger.Info +) + +type Config struct { + LogLevel gormLogger.LogLevel // GORM's Log level: the level of the logs generated by gorm + SlowQueryThreshold time.Duration // Slow SQL Query threshold (use DisableThreshold to disable it) + SlowTransactionThreshold time.Duration // Slow Transaction threshold (use DisableThreshold to disable it) + IgnoreRecordNotFoundError bool // if true, ignore gorm.ErrRecordNotFound error for logger + ParameterizedQueries bool // if true, don't include params in the query execution logs + Colorful bool // log with colors +} + +func (c Config) toGormConfig() gormLogger.Config { + return gormLogger.Config{ + LogLevel: c.LogLevel, + SlowThreshold: c.SlowQueryThreshold, + IgnoreRecordNotFoundError: c.IgnoreRecordNotFoundError, + ParameterizedQueries: c.ParameterizedQueries, + Colorful: c.Colorful, + } +} + +// search in the stacktrace the last file outside gormzap, badaas-orm and gorm +func FindLastCaller(skip int) (string, int, int) { + // +1 because at least one will be inside gorm + // +1 because of this function + for i := skip + 1 + 1; i < 18; i++ { + _, file, line, ok := runtime.Caller(i) + + if !ok { + // we checked in all the stacktrace and none meet the conditions, + return "", 0, 0 + } else if !strings.Contains(file, gormSourceDir) && !strings.Contains(file, badaasORMSourceDir) { + // file outside badaas-orm and gorm + return file, line, i - 1 // -1 to remove this function from the stacktrace + } + } + + return "", 0, 0 +} + +var ( + badaasORMSourceDir string + gormSourceDir = filepath.Join("gorm.io", "gorm") +) + +func init() { + _, file, _, _ := runtime.Caller(0) + // compatible solution to get badaas-orm source directory with various operating systems + badaasORMSourceDir = sourceDir(file) +} + +func sourceDir(file string) string { + loggerDir := filepath.Dir(file) + badaasORMDir := filepath.Dir(loggerDir) + + return filepath.ToSlash(badaasORMDir) + "/" +} diff --git a/orm/logical.go b/orm/logical.go new file mode 100644 index 00000000..82f0fa6a --- /dev/null +++ b/orm/logical.go @@ -0,0 +1,22 @@ +package orm + +import ( + "github.com/ditrit/badaas/orm/condition" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/sql" +) + +// Logical Operators +// ref: https://www.postgresql.org/docs/current/functions-logical.html + +func And[T model.Model](conditions ...condition.WhereCondition[T]) condition.WhereCondition[T] { + return condition.And(conditions...) +} + +func Or[T model.Model](conditions ...condition.WhereCondition[T]) condition.WhereCondition[T] { + return condition.NewConnectionCondition(sql.Or, conditions...) +} + +func Not[T model.Model](conditions ...condition.WhereCondition[T]) condition.WhereCondition[T] { + return condition.NewContainerCondition(sql.Not, conditions...) +} diff --git a/orm/model/models.go b/orm/model/models.go new file mode 100644 index 00000000..502c7a64 --- /dev/null +++ b/orm/model/models.go @@ -0,0 +1,60 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +// supported types for model identifier +type ID interface { + UIntID | UUID + + IsNil() bool +} + +type Model interface { + IsLoaded() bool +} + +// Base Model for gorm +// +// Every model intended to be saved in the database must embed this UUIDModel or UIntModel +// reference: https://gorm.io/docs/models.html#gorm-Model +type UUIDModel struct { + ID UUID `gorm:"primarykey;not null"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (model UUIDModel) IsLoaded() bool { + return !model.ID.IsNil() +} + +func (model *UUIDModel) BeforeCreate(_ *gorm.DB) (err error) { + if model.ID == NilUUID { + model.ID = NewUUID() + } + + return nil +} + +type UIntID uint + +const NilUIntID = 0 + +func (id UIntID) IsNil() bool { + return id == NilUIntID +} + +type UIntModel struct { + ID UIntID `gorm:"primarykey;not null"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (model UIntModel) IsLoaded() bool { + return !model.ID.IsNil() +} diff --git a/orm/model/uuid.go b/orm/model/uuid.go new file mode 100644 index 00000000..06543204 --- /dev/null +++ b/orm/model/uuid.go @@ -0,0 +1,114 @@ +package model + +import ( + "context" + "database/sql/driver" + + "github.com/google/uuid" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" +) + +type UUID uuid.UUID + +var NilUUID = UUID(uuid.Nil) + +func (id UUID) GormDBDataType(db *gorm.DB, _ *schema.Field) string { + switch db.Dialector.Name() { + case "mysql": + return "binary(16)" + case "postgres": + return "uuid" + case "sqlite": + return "varchar(36)" + case "sqlserver": + return "uniqueidentifier" + } + + return "" +} + +func (id UUID) String() string { + return uuid.UUID(id).String() +} + +func (id UUID) URN() string { + return uuid.UUID(id).URN() +} + +func (id UUID) Variant() uuid.Variant { + return uuid.UUID(id).Variant() +} + +func (id UUID) Version() uuid.Version { + return uuid.UUID(id).Version() +} + +func (id UUID) MarshalText() ([]byte, error) { + return uuid.UUID(id).MarshalText() +} + +func (id *UUID) UnmarshalText(data []byte) error { + return (*uuid.UUID)(id).UnmarshalText(data) +} + +func (id UUID) MarshalBinary() ([]byte, error) { + return uuid.UUID(id).MarshalBinary() +} + +func (id *UUID) UnmarshalBinary(data []byte) error { + return (*uuid.UUID)(id).UnmarshalBinary(data) +} + +func (id *UUID) Scan(src interface{}) error { + return (*uuid.UUID)(id).Scan(src) +} + +func (id UUID) IsNil() bool { + return id == NilUUID +} + +func (id UUID) GormValue(_ context.Context, db *gorm.DB) clause.Expr { + if id == NilUUID { + return gorm.Expr("NULL") + } + + switch db.Dialector.Name() { + case "mysql", "sqlserver": + binary, err := id.MarshalBinary() + if err != nil { + _ = db.AddError(err) + return clause.Expr{} + } + + return gorm.Expr("?", binary) + default: + return gorm.Expr("?", id.String()) + } +} + +func (id UUID) Value() (driver.Value, error) { + return uuid.UUID(id).Value() +} + +func (id UUID) Time() uuid.Time { + return uuid.UUID(id).Time() +} + +func (id UUID) ClockSequence() int { + return uuid.UUID(id).ClockSequence() +} + +func NewUUID() UUID { + return UUID(uuid.New()) +} + +func ParseUUID(s string) (UUID, error) { + uid, err := uuid.Parse(s) + if err != nil { + return UUID(uuid.Nil), err + } + + return UUID(uid), nil +} diff --git a/orm/model/uuid_test.go b/orm/model/uuid_test.go new file mode 100644 index 00000000..26ed7f7d --- /dev/null +++ b/orm/model/uuid_test.go @@ -0,0 +1,23 @@ +package model_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "github.com/ditrit/badaas/orm/model" +) + +func TestParseCorrectUUID(t *testing.T) { + uuidString := uuid.New().String() + uuid, err := model.ParseUUID(uuidString) + assert.Nil(t, err) + assert.Equal(t, uuidString, uuid.String()) +} + +func TestParseIncorrectUUID(t *testing.T) { + uid, err := model.ParseUUID("not uuid") + assert.Error(t, err) + assert.Equal(t, model.NilUUID, uid) +} diff --git a/orm/mysql/comparison.go b/orm/mysql/comparison.go new file mode 100644 index 00000000..d3319be2 --- /dev/null +++ b/orm/mysql/comparison.go @@ -0,0 +1,22 @@ +package mysql + +import ( + "github.com/ditrit/badaas/orm/operator" + "github.com/ditrit/badaas/orm/sql" +) + +// Pattern Matching + +// As an extension to standard SQL, MySQL permits LIKE on numeric expressions. +func Like[T string | + int | int8 | int16 | int32 | int64 | + uint | uint8 | uint16 | uint32 | uint64 | + float32 | float64](pattern string, +) operator.Operator[T] { + return operator.NewValueOperator[T](sql.Like, pattern) +} + +// ref: https://dev.mysql.com/doc/refman/8.0/en/regexp.html#operator_regexp +func RegexP(pattern string) operator.Operator[string] { + return operator.NewValueOperator[string](sql.MySQLRegexp, pattern) +} diff --git a/orm/mysql/logical.go b/orm/mysql/logical.go new file mode 100644 index 00000000..dcb76087 --- /dev/null +++ b/orm/mysql/logical.go @@ -0,0 +1,11 @@ +package mysql + +import ( + "github.com/ditrit/badaas/orm/condition" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/sql" +) + +func Xor[T model.Model](conditions ...condition.WhereCondition[T]) condition.WhereCondition[T] { + return condition.NewConnectionCondition(sql.MySQLXor, conditions...) +} diff --git a/orm/operator/errors.go b/orm/operator/errors.go new file mode 100644 index 00000000..d5d28d2d --- /dev/null +++ b/orm/operator/errors.go @@ -0,0 +1,11 @@ +package operator + +import ( + "fmt" + + "github.com/ditrit/badaas/orm/sql" +) + +func operatorError(err error, sqlOperator sql.Operator) error { + return fmt.Errorf("%w; operator: %s", err, sqlOperator.Name()) +} diff --git a/orm/operator/operator.go b/orm/operator/operator.go new file mode 100644 index 00000000..0b140890 --- /dev/null +++ b/orm/operator/operator.go @@ -0,0 +1,25 @@ +package operator + +import "github.com/ditrit/badaas/orm/query" + +type Operator[T any] interface { + // Transform the Operator to a SQL string and a list of values to use in the query + // columnName is used by the operator to determine which is the objective column. + ToSQL(query *query.GormQuery, columnName string) (string, []any, error) + + // This method is necessary to get the compiler to verify + // that an object is of type Operator[T], + // since if no method receives by parameter a type T, + // any other Operator[T2] would also be considered a Operator[T]. + InterfaceVerificationMethod(T) +} + +type DynamicOperator[T any] interface { + Operator[T] + + // Allows to choose which number of join use + // for the value in position "valueNumber" + // when the value is a field and its model is joined more than once. + // Does nothing if the valueNumber is bigger than the amount of values. + SelectJoin(valueNumber, joinNumber uint) DynamicOperator[T] +} diff --git a/orm/operator/operators.go b/orm/operator/operators.go new file mode 100644 index 00000000..97f2db81 --- /dev/null +++ b/orm/operator/operators.go @@ -0,0 +1,164 @@ +package operator + +import ( + "github.com/ditrit/badaas/orm/sql" +) + +// Comparison Operators +// refs: +// - MySQL: https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html +// - PostgreSQL: https://www.postgresql.org/docs/current/functions-comparison.html +// - SQLServer: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/comparison-operators-transact-sql?view=sql-server-ver16 +// - SQLite: https://www.sqlite.org/lang_expr.html + +// EqualTo +// IsNotDistinct must be used in cases where value can be NULL +func Eq[T any](value T) Operator[T] { + return NewValueOperator[T](sql.Eq, value) +} + +// NotEqualTo +// IsDistinct must be used in cases where value can be NULL +func NotEq[T any](value T) Operator[T] { + return NewValueOperator[T](sql.NotEq, value) +} + +// LessThan +func Lt[T any](value T) Operator[T] { + return NewValueOperator[T](sql.Lt, value) +} + +// LessThanOrEqualTo +func LtOrEq[T any](value T) Operator[T] { + return NewValueOperator[T](sql.LtOrEq, value) +} + +// GreaterThan +func Gt[T any](value T) Operator[T] { + return NewValueOperator[T](sql.Gt, value) +} + +// GreaterThanOrEqualTo +func GtOrEq[T any](value T) Operator[T] { + return NewValueOperator[T](sql.GtOrEq, value) +} + +// Comparison Predicates +// refs: +// - MySQL: https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html +// - PostgreSQL: https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE +// - SQLServer: https://learn.microsoft.com/en-us/sql/t-sql/queries/predicates?view=sql-server-ver16 +// - SQLite: https://www.sqlite.org/lang_expr.html + +// Equivalent to v1 < value < v2 +func Between[T any](v1 T, v2 T) Operator[T] { + return newBetweenOperator(sql.Between, v1, v2) +} + +// Equivalent to NOT (v1 < value < v2) +func NotBetween[T any](v1 T, v2 T) Operator[T] { + return newBetweenOperator(sql.NotBetween, v1, v2) +} + +func newBetweenOperator[T any](sqlOperator sql.Operator, v1 T, v2 T) Operator[T] { + operator := NewValueOperator[T](sqlOperator, v1) + return operator.AddOperation(sql.And, v2) +} + +func IsNull[T any]() Operator[T] { + return NewPredicateOperator[T]("IS NULL") +} + +func IsNotNull[T any]() Operator[T] { + return NewPredicateOperator[T]("IS NOT NULL") +} + +// Boolean Comparison Predicates + +// Not supported by: sqlserver +func IsTrue() PredicateOperator[bool] { + return NewPredicateOperator[bool]("IS TRUE") +} + +// Not supported by: sqlserver +func IsNotTrue() Operator[bool] { + return NewPredicateOperator[bool]("IS NOT TRUE") +} + +// Not supported by: sqlserver +func IsFalse() Operator[bool] { + return NewPredicateOperator[bool]("IS FALSE") +} + +// Not supported by: sqlserver +func IsNotFalse() Operator[bool] { + return NewPredicateOperator[bool]("IS NOT FALSE") +} + +// Not supported by: sqlserver, sqlite +func IsUnknown() Operator[bool] { + return NewPredicateOperator[bool]("IS UNKNOWN") +} + +// Not supported by: sqlserver, sqlite +func IsNotUnknown() Operator[bool] { + return NewPredicateOperator[bool]("IS NOT UNKNOWN") +} + +// Not supported by: mysql +func IsDistinct[T any](value T) Operator[T] { + return NewValueOperator[T](sql.IsDistinct, value) +} + +// Not supported by: mysql +func IsNotDistinct[T any](value T) Operator[T] { + return NewValueOperator[T](sql.IsNotDistinct, value) +} + +// Row and Array Comparisons + +func In[T any](values []T) Operator[T] { + return NewValueOperator[T](sql.ArrayIn, values) +} + +func NotIn[T any](values []T) Operator[T] { + return NewValueOperator[T](sql.ArrayNotIn, values) +} + +// Pattern Matching + +type LikeOperator struct { + ValueOperator[string] +} + +func NewLikeOperator(sqlOperator sql.Operator, pattern string) LikeOperator { + return LikeOperator{ + ValueOperator: *NewValueOperator[string](sqlOperator, pattern), + } +} + +func (operator LikeOperator) Escape(escape rune) ValueOperator[string] { + return *operator.AddOperation(sql.Escape, string(escape)) +} + +// Pattern in all databases: +// - An underscore (_) in pattern stands for (matches) any single character. +// - A percent sign (%) matches any sequence of zero or more characters. +// +// Additionally in SQLServer: +// - Square brackets ([ ]) matches any single character within the specified range ([a-f]) or set ([abcdef]). +// - [^] matches any single character not within the specified range ([^a-f]) or set ([^abcdef]). +// +// WARNINGS: +// - SQLite: LIKE is case-insensitive unless case_sensitive_like pragma (https://www.sqlite.org/pragma.html#pragma_case_sensitive_like) is true. +// - SQLServer, MySQL: the case-sensitivity depends on the collation used in compared column. +// - PostgreSQL: LIKE is always case-sensitive, if you want case-insensitive use the ILIKE operator (implemented in psql.ILike) +// +// refs: +// - mysql: https://dev.mysql.com/doc/refman/8.0/en/string-comparison-functions.html#operator_like +// - postgresql: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE +// - sqlserver: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/like-transact-sql?view=sql-server-ver16 +// - sqlite: https://www.sqlite.org/lang_expr.html#like +func Like(pattern string) LikeOperator { + return NewLikeOperator(sql.Like, pattern) +} diff --git a/orm/operator/predicate_operator.go b/orm/operator/predicate_operator.go new file mode 100644 index 00000000..3617ddbe --- /dev/null +++ b/orm/operator/predicate_operator.go @@ -0,0 +1,28 @@ +package operator + +import ( + "fmt" + + "github.com/ditrit/badaas/orm/query" +) + +// Operator that verifies a predicate +// Example: value IS TRUE +type PredicateOperator[T any] struct { + SQLOperator string +} + +func (operator PredicateOperator[T]) InterfaceVerificationMethod(_ T) { + // This method is necessary to get the compiler to verify + // that an object is of type Operator[T] +} + +func (operator PredicateOperator[T]) ToSQL(_ *query.GormQuery, columnName string) (string, []any, error) { + return fmt.Sprintf("%s %s", columnName, operator.SQLOperator), []any{}, nil +} + +func NewPredicateOperator[T any](sqlOperator string) PredicateOperator[T] { + return PredicateOperator[T]{ + SQLOperator: sqlOperator, + } +} diff --git a/orm/operator/value_operator.go b/orm/operator/value_operator.go new file mode 100644 index 00000000..64c64e9e --- /dev/null +++ b/orm/operator/value_operator.go @@ -0,0 +1,100 @@ +package operator + +import ( + "fmt" + + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/orm/sql" +) + +// Operator that compares the value of the column against a fixed value +// If Operations has multiple entries, operations will be nested +// Example (single): value = v1 +// Example (multi): value LIKE v1 ESCAPE v2 +type ValueOperator[T any] struct { + Operations []operation +} + +type operation struct { + SQLOperator sql.Operator + Value any + JoinNumber int +} + +func NewValueOperator[T any](sqlOperator sql.Operator, value any) *ValueOperator[T] { + return new(ValueOperator[T]).AddOperation(sqlOperator, value) +} + +func (operator ValueOperator[T]) InterfaceVerificationMethod(_ T) { + // This method is necessary to get the compiler to verify + // that an object is of type Operator[T] +} + +// Allows to choose which number of join use +// for the operation in position "operationNumber" +// when the value is a field and its model is joined more than once. +// Does nothing if the operationNumber is bigger than the amount of operations. +func (operator *ValueOperator[T]) SelectJoin(operationNumber, joinNumber uint) DynamicOperator[T] { + if operationNumber >= uint(len(operator.Operations)) { + return operator + } + + operationSaved := operator.Operations[operationNumber] + operationSaved.JoinNumber = int(joinNumber) + operator.Operations[operationNumber] = operationSaved + + return operator +} + +func (operator ValueOperator[T]) ToSQL(queryV *query.GormQuery, columnName string) (string, []any, error) { + operationString := columnName + values := []any{} + + // add each operation to the sql + for _, operation := range operator.Operations { + field, isField := operation.Value.(query.IFieldIdentifier) + if isField { + // if the value of the operation is a field, + // verify that this field is concerned by the query + // (a join was performed with the model to which this field belongs) + // and get the alias of the table of this model. + modelTable, err := getModelTable(queryV, field, operation.JoinNumber, operation.SQLOperator) + if err != nil { + return "", nil, err + } + + operationString += fmt.Sprintf( + " %s %s", + operation.SQLOperator, + field.ColumnSQL(queryV, modelTable), + ) + } else { + operationString += " " + operation.SQLOperator.String() + " ?" + values = append(values, operation.Value) + } + } + + return operationString, values, nil +} + +func getModelTable(queryV *query.GormQuery, field query.IFieldIdentifier, joinNumber int, sqlOperator sql.Operator) (query.Table, error) { + table, err := queryV.GetModelTable(field, joinNumber) + if err != nil { + return query.Table{}, operatorError(err, sqlOperator) + } + + return table, nil +} + +func (operator *ValueOperator[T]) AddOperation(sqlOperator sql.Operator, value any) *ValueOperator[T] { + operator.Operations = append( + operator.Operations, + operation{ + Value: value, + SQLOperator: sqlOperator, + JoinNumber: query.UndefinedJoinNumber, + }, + ) + + return operator +} diff --git a/orm/preload/preload.go b/orm/preload/preload.go new file mode 100644 index 00000000..4e181a21 --- /dev/null +++ b/orm/preload/preload.go @@ -0,0 +1,42 @@ +package preload + +import ( + "github.com/ditrit/badaas/orm/errors" + "github.com/ditrit/badaas/orm/model" +) + +func VerifyStructLoaded[T model.Model](toVerify *T) (*T, error) { + if toVerify == nil || !(*toVerify).IsLoaded() { + return nil, errors.ErrRelationNotLoaded + } + + return toVerify, nil +} + +func VerifyPointerLoaded[TModel model.Model, TID model.ID](id *TID, toVerify *TModel) (*TModel, error) { + // when the pointer to the object is nil + // but the id pointer indicates that the relation is not nil + if id != nil && toVerify == nil { + return nil, errors.ErrRelationNotLoaded + } + + return toVerify, nil +} + +func VerifyPointerWithIDLoaded[TModel model.Model, TID model.ID](id TID, toVerify *TModel) (*TModel, error) { + // when the pointer to the object is nil + // but the id indicates that the relation is not nil + if !id.IsNil() && toVerify == nil { + return nil, errors.ErrRelationNotLoaded + } + + return toVerify, nil +} + +func VerifyCollectionLoaded[T model.Model](collection *[]T) ([]T, error) { + if collection == nil { + return nil, errors.ErrRelationNotLoaded + } + + return *collection, nil +} diff --git a/orm/psql/comparison.go b/orm/psql/comparison.go new file mode 100644 index 00000000..386428af --- /dev/null +++ b/orm/psql/comparison.go @@ -0,0 +1,27 @@ +package psql + +import ( + "github.com/ditrit/badaas/orm/operator" + "github.com/ditrit/badaas/orm/sql" +) + +// Pattern Matching + +func ILike(pattern string) operator.Operator[string] { + return operator.NewValueOperator[string](sql.PostgreSQLILike, pattern) +} + +// ref: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-SIMILARTO-REGEXP +func SimilarTo(pattern string) operator.Operator[string] { + return operator.NewValueOperator[string](sql.PostgreSQLSimilarTo, pattern) +} + +// ref: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP +func POSIXMatch(pattern string) operator.Operator[string] { + return operator.NewValueOperator[string](sql.PostgreSQLPosixMatch, pattern) +} + +// ref: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP +func POSIXIMatch(pattern string) operator.Operator[string] { + return operator.NewValueOperator[string](sql.PostgreSQLPosixIMatch, pattern) +} diff --git a/orm/query.go b/orm/query.go new file mode 100644 index 00000000..c5214231 --- /dev/null +++ b/orm/query.go @@ -0,0 +1,147 @@ +package orm + +import ( + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm/condition" + ormErrors "github.com/ditrit/badaas/orm/errors" + "github.com/ditrit/badaas/orm/model" + ormQuery "github.com/ditrit/badaas/orm/query" +) + +type Query[T model.Model] struct { + gormQuery *ormQuery.GormQuery + err error +} + +// Ascending specify an ascending order when retrieving models from database +// joinNumber can be used to select the join in case the field is joined more than once +func (query *Query[T]) Ascending(field ormQuery.IFieldIdentifier, joinNumber ...uint) *Query[T] { + return query.order(field, false, joinNumber) +} + +// Descending specify a descending order when retrieving models from database +// joinNumber can be used to select the join in case the field is joined more than once +func (query *Query[T]) Descending(field ormQuery.IFieldIdentifier, joinNumber ...uint) *Query[T] { + return query.order(field, true, joinNumber) +} + +// Order specify order when retrieving models from database +// if descending is true, the ordering is in descending direction +func (query *Query[T]) order(field ormQuery.IFieldIdentifier, descending bool, joinNumberList []uint) *Query[T] { + err := query.gormQuery.Order(field, descending, getJoinNumber(joinNumberList)) + if err != nil && query.err == nil { + methodName := "Ascending" + if descending { + methodName = "Descending" + } + + query.err = methodError(err, methodName) + } + + return query +} + +// from a list of uint, return the first or UndefinedJoinNumber in case the list is empty +func getJoinNumber(joinNumberList []uint) int { + if len(joinNumberList) == 0 { + return ormQuery.UndefinedJoinNumber + } + + return int(joinNumberList[0]) +} + +// Limit specify the number of models to be retrieved +// +// Limit conditions can be cancelled by using `Limit(-1)` +func (query *Query[T]) Limit(limit int) *Query[T] { + query.gormQuery.Limit(limit) + + return query +} + +// Offset specify the number of models to skip before starting to return the results +// +// Offset conditions can be cancelled by using `Offset(-1)` +// +// Warning: in MySQL Offset can only be used if Limit is also used +func (query *Query[T]) Offset(offset int) *Query[T] { + query.gormQuery.Offset(offset) + + return query +} + +// First finds the first model ordered by primary key, matching given conditions +// or returns gorm.ErrRecordNotFound is if no model does it +func (query *Query[T]) First() (*T, error) { + if query.err != nil { + return nil, query.err + } + + var model *T + + return model, query.gormQuery.First(&model) +} + +// Take finds the first model returned by the database in no specified order, matching given conditions +// or returns gorm.ErrRecordNotFound is if no model does it +func (query *Query[T]) Take() (*T, error) { + if query.err != nil { + return nil, query.err + } + + var model *T + + return model, query.gormQuery.Take(&model) +} + +// Last finds the last model ordered by primary key, matching given conditions +// or returns gorm.ErrRecordNotFound is if no model does it +func (query *Query[T]) Last() (*T, error) { + if query.err != nil { + return nil, query.err + } + + var model *T + + return model, query.gormQuery.Last(&model) +} + +// FindOne finds the only one model that matches given conditions +// or returns error if 0 or more than 1 are found. +func (query *Query[T]) FindOne() (*T, error) { + models, err := query.Find() + if err != nil { + return nil, err + } + + switch { + case len(models) == 1: + return models[0], nil + case len(models) == 0: + return nil, ormErrors.ErrObjectNotFound + default: + return nil, ormErrors.ErrMoreThanOneObjectFound + } +} + +// Find finds all models matching given conditions +func (query *Query[T]) Find() ([]*T, error) { + if query.err != nil { + return nil, query.err + } + + var models []*T + + return models, query.gormQuery.Find(&models) +} + +// Create a Query to which the conditions are applied inside transaction tx +func NewQuery[T model.Model](tx *gorm.DB, conditions ...condition.Condition[T]) *Query[T] { + gormQuery, err := condition.ApplyConditions[T](tx, conditions) + + return &Query[T]{ + gormQuery: gormQuery, + err: err, + } +} diff --git a/orm/query/errors.go b/orm/query/errors.go new file mode 100644 index 00000000..e01704cf --- /dev/null +++ b/orm/query/errors.go @@ -0,0 +1,21 @@ +package query + +import ( + "fmt" + + "github.com/ditrit/badaas/orm/errors" +) + +func fieldModelNotConcernedError(field IFieldIdentifier) error { + return fmt.Errorf("%w; not concerned model: %s", + errors.ErrFieldModelNotConcerned, + field.GetModelType(), + ) +} + +func joinMustBeSelectedError(field IFieldIdentifier) error { + return fmt.Errorf("%w; joined multiple times model: %s", + errors.ErrJoinMustBeSelected, + field.GetModelType(), + ) +} diff --git a/orm/query/field_identifier.go b/orm/query/field_identifier.go new file mode 100644 index 00000000..5b3be485 --- /dev/null +++ b/orm/query/field_identifier.go @@ -0,0 +1,44 @@ +package query + +import ( + "reflect" +) + +type IFieldIdentifier interface { + ColumnName(query *GormQuery, table Table) string + FieldName() string + ColumnSQL(query *GormQuery, table Table) string + GetModelType() reflect.Type +} + +type FieldIdentifier[T any] struct { + Column string + Field string + ColumnPrefix string + ModelType reflect.Type +} + +func (fieldID FieldIdentifier[T]) GetModelType() reflect.Type { + return fieldID.ModelType +} + +// Returns the name of the field identified +func (fieldID FieldIdentifier[T]) FieldName() string { + return fieldID.Field +} + +// Returns the name of the column in which the field is saved in the table +func (fieldID FieldIdentifier[T]) ColumnName(query *GormQuery, table Table) string { + columnName := fieldID.Column + if columnName == "" { + columnName = query.ColumnName(table, fieldID.Field) + } + + // add column prefix and table name once we know the column name + return fieldID.ColumnPrefix + columnName +} + +// Returns the SQL to get the value of the field in the table +func (fieldID FieldIdentifier[T]) ColumnSQL(query *GormQuery, table Table) string { + return table.Alias + "." + fieldID.ColumnName(query, table) +} diff --git a/orm/query/gorm_query.go b/orm/query/gorm_query.go new file mode 100644 index 00000000..ef027ead --- /dev/null +++ b/orm/query/gorm_query.go @@ -0,0 +1,210 @@ +package query + +import ( + "fmt" + "reflect" + "sync" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "github.com/ditrit/badaas/orm/model" +) + +type GormQuery struct { + GormDB *gorm.DB + ConcernedModels map[reflect.Type][]Table +} + +// Order specify order when retrieving models from database. +// +// if descending is true, the ordering is in descending direction. +// +// joinNumber can be used to select the join in case the field is joined more than once. +func (query *GormQuery) Order(field IFieldIdentifier, descending bool, joinNumber int) error { + table, err := query.GetModelTable(field, joinNumber) + if err != nil { + return err + } + + switch query.Dialector() { + case Postgres: + // postgres supports only order by selected fields + query.AddSelect(table, field) + query.GormDB = query.GormDB.Order( + clause.OrderByColumn{ + Column: clause.Column{ + Name: query.getSelectAlias(table, field), + }, + Desc: descending, + }, + ) + + return nil + case SQLServer, SQLite, MySQL: + query.GormDB = query.GormDB.Order( + clause.OrderByColumn{ + Column: clause.Column{ + Name: field.ColumnSQL( + query, + table, + ), + }, + Desc: descending, + }, + ) + + return nil + } + + return nil +} + +// Offset specify the number of records to skip before starting to return the records +// +// Offset conditions can be cancelled by using `Offset(-1)`. +func (query *GormQuery) Offset(offset int) { + query.GormDB = query.GormDB.Offset(offset) +} + +// Limit specify the number of records to be retrieved +// +// Limit conditions can be cancelled by using `Limit(-1)` +func (query *GormQuery) Limit(limit int) { + query.GormDB = query.GormDB.Limit(limit) +} + +// First finds the first record ordered by primary key, matching given conditions +func (query *GormQuery) First(dest any) error { + return query.GormDB.First(dest).Error +} + +// Take finds the first record returned by the database in no specified order, matching given conditions +func (query *GormQuery) Take(dest any) error { + return query.GormDB.Take(dest).Error +} + +// Last finds the last record ordered by primary key, matching given conditions +func (query *GormQuery) Last(dest any) error { + return query.GormDB.Last(dest).Error +} + +// Find finds all models matching given conditions +func (query *GormQuery) Find(dest any) error { + return query.GormDB.Find(dest).Error +} + +func (query *GormQuery) AddSelect(table Table, fieldID IFieldIdentifier) { + query.GormDB.Statement.Selects = append( + query.GormDB.Statement.Selects, + fmt.Sprintf( + "%s.%s AS %s", + table.Alias, + fieldID.ColumnName(query, table), + query.getSelectAlias(table, fieldID), + ), + ) +} + +func (query *GormQuery) getSelectAlias(table Table, fieldID IFieldIdentifier) string { + return fmt.Sprintf( + "\"%[1]s__%[2]s\"", // name used by gorm to load the fields inside the models + table.Alias, + fieldID.ColumnName(query, table), + ) +} + +func (query *GormQuery) Preload(preloadQuery string, args ...interface{}) { + query.GormDB = query.GormDB.Preload(preloadQuery, args...) +} + +func (query *GormQuery) Unscoped() { + query.GormDB = query.GormDB.Unscoped() +} + +func (query *GormQuery) Where(whereQuery interface{}, args ...interface{}) { + query.GormDB = query.GormDB.Where(whereQuery, args...) +} + +func (query *GormQuery) Joins(joinQuery string, args ...interface{}) { + query.GormDB = query.GormDB.Joins(joinQuery, args...) +} + +func (query *GormQuery) AddConcernedModel(model model.Model, table Table) { + tableList, isPresent := query.ConcernedModels[reflect.TypeOf(model)] + if !isPresent { + query.ConcernedModels[reflect.TypeOf(model)] = []Table{table} + } else { + tableList = append(tableList, table) + query.ConcernedModels[reflect.TypeOf(model)] = tableList + } +} + +func (query *GormQuery) GetTables(modelType reflect.Type) []Table { + tableList, isPresent := query.ConcernedModels[modelType] + if !isPresent { + return nil + } + + return tableList +} + +const UndefinedJoinNumber = -1 + +func (query *GormQuery) GetModelTable(field IFieldIdentifier, joinNumber int) (Table, error) { + modelTables := query.GetTables(field.GetModelType()) + if modelTables == nil { + return Table{}, fieldModelNotConcernedError(field) + } + + if len(modelTables) == 1 { + return modelTables[0], nil + } + + if joinNumber == UndefinedJoinNumber { + return Table{}, joinMustBeSelectedError(field) + } + + return modelTables[joinNumber], nil +} + +func (query GormQuery) ColumnName(table Table, fieldName string) string { + return query.GormDB.NamingStrategy.ColumnName(table.Name, fieldName) +} + +type Dialector string + +const ( + Postgres Dialector = "postgres" + MySQL Dialector = "mysql" + SQLite Dialector = "sqlite" + SQLServer Dialector = "sqlserver" +) + +func (query GormQuery) Dialector() Dialector { + return Dialector(query.GormDB.Dialector.Name()) +} + +func NewGormQuery(db *gorm.DB, initialModel model.Model, initialTable Table) *GormQuery { + query := &GormQuery{ + GormDB: db.Select(initialTable.Name + ".*"), + ConcernedModels: map[reflect.Type][]Table{}, + } + + query.AddConcernedModel(initialModel, initialTable) + + return query +} + +// Get the name of the table in "db" in which the data for "entity" is saved +// returns error is table name can not be found by gorm, +// probably because the type of "entity" is not registered using AddModel +func getTableName(db *gorm.DB, entity any) (string, error) { + schemaName, err := schema.Parse(entity, &sync.Map{}, db.NamingStrategy) + if err != nil { + return "", err + } + + return schemaName.Table, nil +} diff --git a/orm/query/table.go b/orm/query/table.go new file mode 100644 index 00000000..11cb52a9 --- /dev/null +++ b/orm/query/table.go @@ -0,0 +1,53 @@ +package query + +import ( + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm/model" +) + +type Table struct { + Name string + Alias string + Initial bool +} + +// Returns true if the Table is the initial table in a query +func (t Table) IsInitial() bool { + return t.Initial +} + +// Returns the related Table corresponding to the model +func (t Table) DeliverTable(query *GormQuery, model model.Model, relationName string) (Table, error) { + // get the name of the table for the model + tableName, err := getTableName(query.GormDB, model) + if err != nil { + return Table{}, err + } + + // add a suffix to avoid tables with the same name when joining + // the same table more than once + tableAlias := relationName + if !t.IsInitial() { + tableAlias = t.Alias + "__" + relationName + } + + return Table{ + Name: tableName, + Alias: tableAlias, + Initial: false, + }, nil +} + +func NewTable(db *gorm.DB, model model.Model) (Table, error) { + initialTableName, err := getTableName(db, model) + if err != nil { + return Table{}, err + } + + return Table{ + Name: initialTableName, + Alias: initialTableName, + Initial: true, + }, nil +} diff --git a/orm/sql/operators.go b/orm/sql/operators.go new file mode 100644 index 00000000..0f259714 --- /dev/null +++ b/orm/sql/operators.go @@ -0,0 +1,95 @@ +package sql + +type Operator uint + +const ( + Eq Operator = iota + NotEq + Lt + LtOrEq + Gt + GtOrEq + Between + NotBetween + IsDistinct + IsNotDistinct + Like + Escape + ArrayIn + ArrayNotIn + And + Or + Not + // mysql + MySQLXor + MySQLRegexp + // postgresql + PostgreSQLILike + PostgreSQLSimilarTo + PostgreSQLPosixMatch + PostgreSQLPosixIMatch + // sqlite + SQLiteGlob +) + +func (op Operator) String() string { + return operatorToSQL[op] +} + +var operatorToSQL = map[Operator]string{ + Eq: "=", + NotEq: "<>", + Lt: "<", + LtOrEq: "<=", + Gt: ">", + GtOrEq: ">=", + Between: "BETWEEN", + NotBetween: "NOT BETWEEN", + IsDistinct: "IS DISTINCT FROM", + IsNotDistinct: "IS NOT DISTINCT FROM", + Like: "LIKE", + Escape: "ESCAPE", + ArrayIn: "IN", + ArrayNotIn: "NOT IN", + And: "AND", + Or: "OR", + Not: "NOT", + MySQLXor: "XOR", + MySQLRegexp: "REGEXP", + PostgreSQLILike: "ILIKE", + PostgreSQLSimilarTo: "SIMILAR TO", + PostgreSQLPosixMatch: "~", + PostgreSQLPosixIMatch: "~*", + SQLiteGlob: "GLOB", +} + +func (op Operator) Name() string { + return operatorToName[op] +} + +var operatorToName = map[Operator]string{ + Eq: "Eq", + NotEq: "NotEq", + Lt: "Lt", + LtOrEq: "LtOrEq", + Gt: "Gt", + GtOrEq: "GtOrEq", + Between: "Between", + NotBetween: "NotBetween", + IsDistinct: "IsDistinct", + IsNotDistinct: "IsNotDistinct", + Like: "Like", + Escape: "Escape", + ArrayIn: "ArrayIn", + ArrayNotIn: "ArrayNotIn", + And: "And", + Or: "Or", + Not: "Not", + MySQLXor: "mysql.Xor", + MySQLRegexp: "mysql.Regexp", + PostgreSQLILike: "psql.ILike", + PostgreSQLSimilarTo: "psql.SimilarTo", + PostgreSQLPosixMatch: "psql.PosixMatch", + PostgreSQLPosixIMatch: "psql.PosixIMatch", + SQLiteGlob: "sqlite.Glob", +} diff --git a/orm/sqlite/comparison.go b/orm/sqlite/comparison.go new file mode 100644 index 00000000..d86d6db6 --- /dev/null +++ b/orm/sqlite/comparison.go @@ -0,0 +1,11 @@ +package sqlite + +import ( + "github.com/ditrit/badaas/orm/operator" + "github.com/ditrit/badaas/orm/sql" +) + +// ref: https://www.sqlie.org/lang_expr.html#like +func Glob(pattern string) operator.Operator[string] { + return operator.NewValueOperator[string](sql.SQLiteGlob, pattern) +} diff --git a/orm/transaction.go b/orm/transaction.go new file mode 100644 index 00000000..773496cb --- /dev/null +++ b/orm/transaction.go @@ -0,0 +1,44 @@ +package orm + +import ( + "context" + "database/sql" + "time" + + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm/logger" +) + +// Executes the function "toExec" inside a transaction +// The transaction is automatically rolled back in case "toExec" returns an error +// opts can be used to pass arguments to the transaction +func Transaction[RT any]( + db *gorm.DB, + toExec func(*gorm.DB) (RT, error), + opts ...*sql.TxOptions, +) (RT, error) { + begin := time.Now() + + var returnValue RT + + err := db.Transaction( + func(tx *gorm.DB) error { + var err error + returnValue, err = toExec(tx) + + return err + }, + opts..., + ) + if err != nil { + return *new(RT), err + } + + loggerInterface, isLoggerInterface := db.Logger.(logger.Interface) + if isLoggerInterface { + loggerInterface.TraceTransaction(context.Background(), begin) + } + + return returnValue, nil +} diff --git a/orm/unsafe/condition.go b/orm/unsafe/condition.go new file mode 100644 index 00000000..c2cf86ab --- /dev/null +++ b/orm/unsafe/condition.go @@ -0,0 +1,45 @@ +package unsafe + +import ( + "fmt" + + "github.com/ditrit/badaas/orm/condition" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/orm/query" +) + +// Condition that can be used to express conditions that are not supported (yet?) by badaas-orm +// Example: table1.columnX = table2.columnY +type unsafeCondition[T model.Model] struct { + SQLCondition string + Values []any +} + +func (unsafeCondition unsafeCondition[T]) InterfaceVerificationMethod(_ T) { + // This method is necessary to get the compiler to verify + // that an object is of type Condition[T] +} + +func (unsafeCondition unsafeCondition[T]) ApplyTo(queryV *query.GormQuery, table query.Table) error { + return condition.ApplyWhereCondition[T](unsafeCondition, queryV, table) +} + +func (unsafeCondition unsafeCondition[T]) GetSQL(_ *query.GormQuery, table query.Table) (string, []any, error) { + return fmt.Sprintf( + unsafeCondition.SQLCondition, + table.Alias, + ), unsafeCondition.Values, nil +} + +func (unsafeCondition unsafeCondition[T]) AffectsDeletedAt() bool { + return false +} + +// Condition that can be used to express conditions that are not supported (yet?) by badaas-orm +// Example: table1.columnX = table2.columnY +func NewCondition[T model.Model](sqlCondition string, values ...any) condition.Condition[T] { + return unsafeCondition[T]{ + SQLCondition: sqlCondition, + Values: values, + } +} diff --git a/orm/unsafe/operators.go b/orm/unsafe/operators.go new file mode 100644 index 00000000..d1d09430 --- /dev/null +++ b/orm/unsafe/operators.go @@ -0,0 +1,85 @@ +package unsafe + +import ( + "github.com/ditrit/badaas/orm/operator" + "github.com/ditrit/badaas/orm/sql" +) + +// Comparison Operators +// refs: +// - MySQL: https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html +// - PostgreSQL: https://www.postgresql.org/docs/current/functions-comparison.html +// - SQLServer: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/comparison-operators-transact-sql?view=sql-server-ver16 +// - SQLite: https://www.sqlite.org/lang_expr.html + +// EqualTo +func Eq[T any](value any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.Eq, value) +} + +// NotEqualTo +func NotEq[T any](value any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.NotEq, value) +} + +// LessThan +func Lt[T any](value any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.Lt, value) +} + +// LessThanOrEqualTo +func LtOrEq[T any](value any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.LtOrEq, value) +} + +// GreaterThan +func Gt[T any](value any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.Gt, value) +} + +// GreaterThanOrEqualTo +func GtOrEq[T any](value any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.GtOrEq, value) +} + +// Comparison Predicates +// refs: +// - MySQL: https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html +// - PostgreSQL: https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE +// - SQLServer: https://learn.microsoft.com/en-us/sql/t-sql/queries/predicates?view=sql-server-ver16 +// - SQLite: https://www.sqlite.org/lang_expr.html + +// Equivalent to v1 < value < v2 +func Between[T any](v1, v2 any) operator.DynamicOperator[T] { + return newBetweenOperator[T](sql.Between, v1, v2) +} + +// Equivalent to NOT (v1 < value < v2) +func NotBetween[T any](v1, v2 any) operator.DynamicOperator[T] { + return newBetweenOperator[T](sql.NotBetween, v1, v2) +} + +func newBetweenOperator[T any](sqlOperator sql.Operator, v1, v2 any) operator.DynamicOperator[T] { + operator := operator.NewValueOperator[T](sqlOperator, v1) + return operator.AddOperation(sql.And, v2) +} + +// Not supported by: mysql +func IsDistinct[T any](value any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.IsDistinct, value) +} + +// Not supported by: mysql +func IsNotDistinct[T any](value any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.IsNotDistinct, value) +} + +// Row and Array Comparisons + +func ArrayIn[T any](values ...any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.ArrayIn, values) +} + +func ArrayNotIn[T any](values ...any) operator.DynamicOperator[T] { + return operator.NewValueOperator[T](sql.ArrayNotIn, values) +} diff --git a/persistence/ModuleFx.go b/persistence/ModuleFx.go index d99d1c02..8db596c2 100644 --- a/persistence/ModuleFx.go +++ b/persistence/ModuleFx.go @@ -1,11 +1,10 @@ package persistence import ( - "github.com/ditrit/badaas/persistence/gormdatabase" - "github.com/ditrit/badaas/persistence/models" - "github.com/ditrit/badaas/persistence/repository" - "github.com/google/uuid" "go.uber.org/fx" + + "github.com/ditrit/badaas/persistence/database" + "github.com/ditrit/badaas/persistence/gormfx" ) // PersistanceModule for fx @@ -13,14 +12,11 @@ import ( // Provides: // // - The database connection -// -// - The repositories +// - badaas-orm auto-migration var PersistanceModule = fx.Module( "persistence", // Database connection - fx.Provide(gormdatabase.CreateDatabaseConnectionFromConfiguration), - - //repositories - fx.Provide(repository.NewCRUDRepository[models.Session, uuid.UUID]), - fx.Provide(repository.NewCRUDRepository[models.User, uuid.UUID]), + fx.Provide(database.SetupDatabaseConnection), + // auto-migrate + gormfx.AutoMigrate, ) diff --git a/persistence/database/db.go b/persistence/database/db.go new file mode 100644 index 00000000..4a638a38 --- /dev/null +++ b/persistence/database/db.go @@ -0,0 +1,94 @@ +package database + +import ( + "context" + "time" + + "go.uber.org/zap" + "gorm.io/driver/postgres" + "gorm.io/gorm" + + "github.com/ditrit/badaas/configuration" + "github.com/ditrit/badaas/orm" + "github.com/ditrit/badaas/orm/logger" + "github.com/ditrit/badaas/orm/logger/gormzap" +) + +// Create the dsn string from the configuration +func createDialectorFromConf(databaseConfiguration configuration.DatabaseConfiguration) gorm.Dialector { + return postgres.Open(orm.CreatePostgreSQLDSN( + databaseConfiguration.GetHost(), + databaseConfiguration.GetUsername(), + databaseConfiguration.GetPassword(), + databaseConfiguration.GetSSLMode(), + databaseConfiguration.GetDBName(), + databaseConfiguration.GetPort(), + )) +} + +// Creates the database object with using the database configuration and exec the setup +func SetupDatabaseConnection( + zapLogger *zap.Logger, + databaseConfiguration configuration.DatabaseConfiguration, + loggerConfiguration configuration.LoggerConfiguration, +) (*gorm.DB, error) { + return OpenWithRetry( + createDialectorFromConf(databaseConfiguration), + gormzap.New(zapLogger, logger.Config{ + LogLevel: loggerConfiguration.GetLogLevel(), + SlowQueryThreshold: loggerConfiguration.GetSlowQueryThreshold(), + SlowTransactionThreshold: loggerConfiguration.GetSlowTransactionThreshold(), + IgnoreRecordNotFoundError: loggerConfiguration.GetIgnoreRecordNotFoundError(), + ParameterizedQueries: loggerConfiguration.GetParameterizedQueries(), + }), + databaseConfiguration.GetRetry(), + databaseConfiguration.GetRetryTime(), + ) +} + +func OpenWithRetry( + dialector gorm.Dialector, + logger logger.Interface, + connectionTries uint, + retryTime time.Duration, +) (*gorm.DB, error) { + var err error + + var gormDB *gorm.DB + + for retryNumber := uint(0); retryNumber < connectionTries; retryNumber++ { + gormDB, err = orm.Open( + dialector, + &gorm.Config{ + Logger: logger, + }, + ) + + if err == nil { + logger.Info(context.Background(), "Database connection is active") + + return gormDB, nil + } + + // there are more retries + if retryNumber < connectionTries-1 { + logger.Info( + context.Background(), + "Database connection failed with error %q, retrying %d/%d in %s", + err.Error(), + retryNumber+1+1, // +1 for counting from 1 and +1 for next iteration + connectionTries, + retryTime, + ) + time.Sleep(retryTime) + } else { + logger.Error( + context.Background(), + "Database connection failed with error %q", + err.Error(), + ) + } + } + + return nil, err +} diff --git a/persistence/gormdatabase/db.go b/persistence/gormdatabase/db.go deleted file mode 100644 index ddd5a22b..00000000 --- a/persistence/gormdatabase/db.go +++ /dev/null @@ -1,98 +0,0 @@ -package gormdatabase - -import ( - "fmt" - "time" - - "github.com/ditrit/badaas/configuration" - "github.com/ditrit/badaas/persistence/gormdatabase/gormzap" - "github.com/ditrit/badaas/persistence/models" - "go.uber.org/zap" - "gorm.io/driver/postgres" - "gorm.io/gorm" -) - -// Create the dsn string from the configuration -func createDsnFromConf(databaseConfiguration configuration.DatabaseConfiguration) string { - dsn := createDsn( - databaseConfiguration.GetHost(), - databaseConfiguration.GetUsername(), - databaseConfiguration.GetPassword(), - databaseConfiguration.GetSSLMode(), - databaseConfiguration.GetDBName(), - databaseConfiguration.GetPort(), - ) - return dsn -} - -// Create the dsn strings with the provided args -func createDsn(host, username, password, sslmode, dbname string, port int) string { - return fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", - username, password, host, port, sslmode, dbname, - ) -} - -// Initialize the database with using the database configuration -func CreateDatabaseConnectionFromConfiguration(logger *zap.Logger, databaseConfiguration configuration.DatabaseConfiguration) (*gorm.DB, error) { - dsn := createDsnFromConf(databaseConfiguration) - var err error - var database *gorm.DB - for numberRetry := uint(0); numberRetry < databaseConfiguration.GetRetry(); numberRetry++ { - database, err = initializeDBFromDsn(dsn, logger) - if err == nil { - logger.Sugar().Debugf("Database connection is active") - err = AutoMigrate(logger, database) - if err != nil { - logger.Error("migration failed") - return nil, err - } - logger.Info("AutoMigration was executed successfully") - return database, err - } - logger.Sugar().Debugf("Database connection failed with error %q", err.Error()) - logger.Sugar().Debugf("Retrying database connection %d/%d in %s", - numberRetry+1, databaseConfiguration.GetRetry(), databaseConfiguration.GetRetryTime().String()) - time.Sleep(databaseConfiguration.GetRetryTime()) - } - return nil, err -} - -// Initialize the database with the dsn string -func initializeDBFromDsn(dsn string, logger *zap.Logger) (*gorm.DB, error) { - database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ - Logger: gormzap.New(logger), - }) - - if err != nil { - return nil, err - } - - rawDatabase, err := database.DB() - if err != nil { - return nil, err - } - // ping the underlying database - err = rawDatabase.Ping() - if err != nil { - return nil, err - } - return database, nil -} - -// Migrate the database using gorm [https://gorm.io/docs/migration.html#Auto-Migration] -func autoMigrate(database *gorm.DB, listOfDatabaseTables []any) error { - err := database.AutoMigrate(listOfDatabaseTables...) - if err != nil { - return err - } - return nil -} - -// Run the automigration -func AutoMigrate(logger *zap.Logger, database *gorm.DB) error { - err := autoMigrate(database, models.ListOfTables) - if err != nil { - return err - } - return nil -} diff --git a/persistence/gormdatabase/db_test.go b/persistence/gormdatabase/db_test.go deleted file mode 100644 index 05eef996..00000000 --- a/persistence/gormdatabase/db_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package gormdatabase - -import ( - "testing" - - configurationmocks "github.com/ditrit/badaas/mocks/configuration" - "github.com/stretchr/testify/assert" -) - -func TestCreateDsnFromconf(t *testing.T) { - conf := configurationmocks.NewDatabaseConfiguration(t) - conf.On("GetPort").Return(1225) - conf.On("GetHost").Return("192.168.2.5") - conf.On("GetDBName").Return("badaas_db") - conf.On("GetUsername").Return("username") - conf.On("GetPassword").Return("password") - conf.On("GetSSLMode").Return("disable") - assert.Equal(t, "user=username password=password host=192.168.2.5 port=1225 sslmode=disable dbname=badaas_db", - createDsnFromConf(conf)) -} - -func TestCreateDsn(t *testing.T) { - assert.Equal(t, - "user=username password=password host=192.168.2.5 port=1225 sslmode=disable dbname=badaas_db", - createDsn( - "192.168.2.5", - "username", - "password", - "disable", - "badaas_db", - 1225, - ), - "no dsn should be empty", - ) -} diff --git a/persistence/gormdatabase/gormzap/gormzap.go b/persistence/gormdatabase/gormzap/gormzap.go deleted file mode 100644 index 0f7b9563..00000000 --- a/persistence/gormdatabase/gormzap/gormzap.go +++ /dev/null @@ -1,114 +0,0 @@ -package gormzap - -import ( - "context" - "errors" - "path/filepath" - "runtime" - "strings" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" - gormlogger "gorm.io/gorm/logger" -) - -// This type implement the [gorm.io/gorm/logger.Interface] interface. -// It is to be used as a replacement for the original logger -type Logger struct { - ZapLogger *zap.Logger - LogLevel gormlogger.LogLevel - SlowThreshold time.Duration - SkipCallerLookup bool - IgnoreRecordNotFoundError bool -} - -// The constructor of the gormzap logger -func New(zapLogger *zap.Logger) gormlogger.Interface { - return Logger{ - ZapLogger: zapLogger, - LogLevel: gormlogger.Info, - SlowThreshold: 100 * time.Millisecond, - SkipCallerLookup: true, - IgnoreRecordNotFoundError: true, - } -} - -// Set the global instance of gorm to the local instance of gormzap logger -func (l Logger) SetAsDefault() { - gormlogger.Default = l -} - -// Set the log mode to the value passed as argument -func (l Logger) LogMode(level gormlogger.LogLevel) gormlogger.Interface { - return Logger{ - ZapLogger: l.ZapLogger, - SlowThreshold: l.SlowThreshold, - LogLevel: level, - SkipCallerLookup: l.SkipCallerLookup, - IgnoreRecordNotFoundError: l.IgnoreRecordNotFoundError, - } -} - -// log info -func (l Logger) Info(_ context.Context, str string, args ...interface{}) { - if l.LogLevel < gormlogger.Info { - return - } - l.logger().Sugar().Debugf(str, args...) -} - -// log warning -func (l Logger) Warn(_ context.Context, str string, args ...interface{}) { - if l.LogLevel < gormlogger.Warn { - return - } - l.logger().Sugar().Warnf(str, args...) -} - -// log an error -func (l Logger) Error(_ context.Context, str string, args ...interface{}) { - if l.LogLevel < gormlogger.Error { - return - } - l.logger().Sugar().Errorf(str, args...) -} - -// log a trace -func (l Logger) Trace(_ context.Context, begin time.Time, fc func() (string, int64), err error) { - if l.LogLevel <= 0 { - return - } - - elapsed := time.Since(begin) - sql, rows := fc() - switch { - case err != nil && l.LogLevel >= gormlogger.Error && (!l.IgnoreRecordNotFoundError || !errors.Is(err, gorm.ErrRecordNotFound)): - l.logger().Error("trace", zap.Error(err), zap.Duration("elapsed", elapsed), zap.Int64("rows", rows), zap.String("sql", sql)) - case l.SlowThreshold != 0 && elapsed > l.SlowThreshold && l.LogLevel >= gormlogger.Warn: - l.logger().Warn("trace", zap.Duration("elapsed", elapsed), zap.Int64("rows", rows), zap.String("sql", sql)) - case l.LogLevel >= gormlogger.Info: - l.logger().Debug("trace", zap.Duration("elapsed", elapsed), zap.Int64("rows", rows), zap.String("sql", sql)) - } -} - -var ( - gormPackage = filepath.Join("gorm.io", "gorm") - zapgormPackage = filepath.Join("github.com", "ditrit", "badaas", "persistence", "gormdatabase", "gormzap") -) - -// return a logger that log the right caller -func (l Logger) logger() *zap.Logger { - for i := 2; i < 15; i++ { - _, file, _, ok := runtime.Caller(i) - switch { - case !ok: - case strings.HasSuffix(file, "_test.go"): - case strings.Contains(file, gormPackage): - case strings.Contains(file, zapgormPackage): - default: - return l.ZapLogger.WithOptions(zap.AddCallerSkip(i)) - } - } - return l.ZapLogger -} diff --git a/persistence/gormdatabase/helpers.go b/persistence/gormdatabase/helpers.go deleted file mode 100644 index c71650a5..00000000 --- a/persistence/gormdatabase/helpers.go +++ /dev/null @@ -1,17 +0,0 @@ -package gormdatabase - -import "github.com/jackc/pgconn" - -func IsDuplicateKeyError(err error) bool { - // unique_violation code is equals to 23505 - return isPostgresError(err, "23505") -} - -func isPostgresError(err error, errCode string) bool { - postgresError, ok := err.(*pgconn.PgError) - if ok { - return postgresError.Code == errCode - - } - return false -} diff --git a/persistence/gormdatabase/helpers_test.go b/persistence/gormdatabase/helpers_test.go deleted file mode 100644 index 59cb2d6e..00000000 --- a/persistence/gormdatabase/helpers_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package gormdatabase - -import ( - "errors" - "testing" - - "github.com/jackc/pgconn" - "github.com/stretchr/testify/assert" -) - -func TestIsDuplicateError(t *testing.T) { - assert.False(t, IsDuplicateKeyError(errors.New("voila"))) - assert.False(t, IsDuplicateKeyError(&pgconn.PgError{ - Code: "235252551514", - })) - assert.True(t, IsDuplicateKeyError(&pgconn.PgError{ - Code: "23505", - })) -} - -func Test_isPostgresError(t *testing.T) { - var postgresErrorAsError error = &pgconn.PgError{Code: "1234"} - assert.True(t, isPostgresError( - postgresErrorAsError, - "1234", - )) - postgresErrorAsError = &pgconn.PgError{Code: ""} - assert.False(t, isPostgresError( - postgresErrorAsError, - "1234", - )) - assert.False(t, isPostgresError(errors.New("a classic error"), "1234")) -} diff --git a/persistence/gormfx/ModuleFx.go b/persistence/gormfx/ModuleFx.go new file mode 100644 index 00000000..35689117 --- /dev/null +++ b/persistence/gormfx/ModuleFx.go @@ -0,0 +1,32 @@ +package gormfx + +import ( + "github.com/elliotchance/pie/v2" + "go.uber.org/fx" + "gorm.io/gorm" +) + +type GetModelsResult struct { + fx.Out + + Models []any `group:"modelsTables"` +} + +var AutoMigrate = fx.Module( + "AutoMigrate", + fx.Invoke( + fx.Annotate( + autoMigrate, + fx.ParamTags(`group:"modelsTables"`), + ), + ), +) + +func autoMigrate(modelsLists [][]any, db *gorm.DB) error { + if len(modelsLists) > 0 { + allModels := pie.Flat(modelsLists) + return db.AutoMigrate(allModels...) + } + + return nil +} diff --git a/persistence/models/BaseModel.go b/persistence/models/BaseModel.go deleted file mode 100644 index acf3c2e0..00000000 --- a/persistence/models/BaseModel.go +++ /dev/null @@ -1,19 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" - "gorm.io/gorm" -) - -// Base Model for gorm -// -// Every model intended to be saved in the database must embed this BaseModel -// reference: https://gorm.io/docs/models.html#gorm-Model -type BaseModel struct { - ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4()"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` -} diff --git a/persistence/models/ProductInfo.go b/persistence/models/ProductInfo.go deleted file mode 100644 index 42040ce5..00000000 --- a/persistence/models/ProductInfo.go +++ /dev/null @@ -1,7 +0,0 @@ -package models - -// Describe the current BADAAS instance -type BadaasServerInfo struct { - Status string `json:"status"` - Version string `json:"version"` -} diff --git a/persistence/models/Session.go b/persistence/models/Session.go index 89bdb1b7..2ea2acb3 100644 --- a/persistence/models/Session.go +++ b/persistence/models/Session.go @@ -3,14 +3,22 @@ package models import ( "time" - "github.com/google/uuid" + "github.com/ditrit/badaas/orm/model" ) // Represent a user session type Session struct { - BaseModel - UserID uuid.UUID `gorm:"not null"` - ExpiresAt time.Time `gorm:"not null"` + model.UUIDModel + UserID model.UUID `gorm:"not null"` + ExpiresAt time.Time `gorm:"not null"` +} + +// Create a new session +func NewSession(userID model.UUID, sessionDuration time.Duration) *Session { + return &Session{ + UserID: userID, + ExpiresAt: time.Now().Add(sessionDuration), + } } // Return true is expired @@ -22,10 +30,3 @@ func (session *Session) IsExpired() bool { func (session *Session) CanBeRolled(rollInterval time.Duration) bool { return time.Now().After(session.ExpiresAt.Add(-rollInterval)) } - -// Return the pluralized table name -// -// Satisfie the Tabler interface -func (Session) TableName() string { - return "sessions" -} diff --git a/persistence/models/Session_test.go b/persistence/models/Session_test.go index a1fdd240..fb92d2a2 100644 --- a/persistence/models/Session_test.go +++ b/persistence/models/Session_test.go @@ -4,10 +4,18 @@ import ( "testing" "time" - "github.com/ditrit/badaas/persistence/models" "github.com/stretchr/testify/assert" + + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/persistence/models" ) +func TestNewSession(t *testing.T) { + sessionInstance := models.NewSession(model.NilUUID, time.Second) + assert.NotNil(t, sessionInstance) + assert.Equal(t, model.NilUUID, sessionInstance.UserID) +} + func TestExpired(t *testing.T) { sessionInstance := &models.Session{ ExpiresAt: time.Now().Add(time.Second), @@ -26,7 +34,3 @@ func TestCanBeRolled(t *testing.T) { time.Sleep(400 * time.Millisecond) assert.True(t, sessionInstance.CanBeRolled(sessionDuration)) } - -func TestTableName(t *testing.T) { - assert.Equal(t, "sessions", models.Session{}.TableName()) -} diff --git a/persistence/models/Tabler.go b/persistence/models/Tabler.go deleted file mode 100644 index eac89ab8..00000000 --- a/persistence/models/Tabler.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -var ListOfTables = []any{ - User{}, - Session{}, -} - -// The interface "type" need to implement to be considered models -type Tabler interface { - // pluralized name - TableName() string -} diff --git a/persistence/models/User.go b/persistence/models/User.go index d65aa425..fefe1579 100644 --- a/persistence/models/User.go +++ b/persistence/models/User.go @@ -1,18 +1,15 @@ package models +import ( + "github.com/ditrit/badaas/orm/model" +) + // Represents a user type User struct { - BaseModel + model.UUIDModel Username string `gorm:"not null"` Email string `gorm:"unique;not null"` // password hash Password []byte `gorm:"not null"` } - -// Return the pluralized table name -// -// Satisfie the Tabler interface -func (User) TableName() string { - return "users" -} diff --git a/persistence/models/conditions/orm.go b/persistence/models/conditions/orm.go new file mode 100644 index 00000000..28853ae7 --- /dev/null +++ b/persistence/models/conditions/orm.go @@ -0,0 +1,3 @@ +package conditions + +//go:generate badaas-cli gen conditions ../ diff --git a/persistence/models/conditions/session_conditions.go b/persistence/models/conditions/session_conditions.go new file mode 100644 index 00000000..c9b2c7ed --- /dev/null +++ b/persistence/models/conditions/session_conditions.go @@ -0,0 +1,73 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/persistence/models" + "reflect" + "time" +) + +var sessionType = reflect.TypeOf(*new(models.Session)) + +func (sessionConditions sessionConditions) IdIs() orm.FieldIs[models.Session, model.UUID] { + return orm.FieldIs[models.Session, model.UUID]{FieldID: sessionConditions.ID} +} +func (sessionConditions sessionConditions) CreatedAtIs() orm.FieldIs[models.Session, time.Time] { + return orm.FieldIs[models.Session, time.Time]{FieldID: sessionConditions.CreatedAt} +} +func (sessionConditions sessionConditions) UpdatedAtIs() orm.FieldIs[models.Session, time.Time] { + return orm.FieldIs[models.Session, time.Time]{FieldID: sessionConditions.UpdatedAt} +} +func (sessionConditions sessionConditions) DeletedAtIs() orm.FieldIs[models.Session, time.Time] { + return orm.FieldIs[models.Session, time.Time]{FieldID: sessionConditions.DeletedAt} +} +func (sessionConditions sessionConditions) UserIdIs() orm.FieldIs[models.Session, model.UUID] { + return orm.FieldIs[models.Session, model.UUID]{FieldID: sessionConditions.UserID} +} +func (sessionConditions sessionConditions) ExpiresAtIs() orm.FieldIs[models.Session, time.Time] { + return orm.FieldIs[models.Session, time.Time]{FieldID: sessionConditions.ExpiresAt} +} + +type sessionConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + UserID query.FieldIdentifier[model.UUID] + ExpiresAt query.FieldIdentifier[time.Time] +} + +var Session = sessionConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: sessionType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: sessionType, + }, + ExpiresAt: query.FieldIdentifier[time.Time]{ + Field: "ExpiresAt", + ModelType: sessionType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: sessionType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: sessionType, + }, + UserID: query.FieldIdentifier[model.UUID]{ + Field: "UserID", + ModelType: sessionType, + }, +} + +func (sessionConditions sessionConditions) Preload() condition.Condition[models.Session] { + return condition.NewPreloadCondition[models.Session](sessionConditions.ID, sessionConditions.CreatedAt, sessionConditions.UpdatedAt, sessionConditions.DeletedAt, sessionConditions.UserID, sessionConditions.ExpiresAt) +} diff --git a/persistence/models/conditions/user_conditions.go b/persistence/models/conditions/user_conditions.go new file mode 100644 index 00000000..85997f97 --- /dev/null +++ b/persistence/models/conditions/user_conditions.go @@ -0,0 +1,81 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/persistence/models" + "reflect" + "time" +) + +var userType = reflect.TypeOf(*new(models.User)) + +func (userConditions userConditions) IdIs() orm.FieldIs[models.User, model.UUID] { + return orm.FieldIs[models.User, model.UUID]{FieldID: userConditions.ID} +} +func (userConditions userConditions) CreatedAtIs() orm.FieldIs[models.User, time.Time] { + return orm.FieldIs[models.User, time.Time]{FieldID: userConditions.CreatedAt} +} +func (userConditions userConditions) UpdatedAtIs() orm.FieldIs[models.User, time.Time] { + return orm.FieldIs[models.User, time.Time]{FieldID: userConditions.UpdatedAt} +} +func (userConditions userConditions) DeletedAtIs() orm.FieldIs[models.User, time.Time] { + return orm.FieldIs[models.User, time.Time]{FieldID: userConditions.DeletedAt} +} +func (userConditions userConditions) UsernameIs() orm.StringFieldIs[models.User] { + return orm.StringFieldIs[models.User]{FieldIs: orm.FieldIs[models.User, string]{FieldID: userConditions.Username}} +} +func (userConditions userConditions) EmailIs() orm.StringFieldIs[models.User] { + return orm.StringFieldIs[models.User]{FieldIs: orm.FieldIs[models.User, string]{FieldID: userConditions.Email}} +} +func (userConditions userConditions) PasswordIs() orm.FieldIs[models.User, []uint8] { + return orm.FieldIs[models.User, []uint8]{FieldID: userConditions.Password} +} + +type userConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Username query.FieldIdentifier[string] + Email query.FieldIdentifier[string] + Password query.FieldIdentifier[[]uint8] +} + +var User = userConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: userType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: userType, + }, + Email: query.FieldIdentifier[string]{ + Field: "Email", + ModelType: userType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: userType, + }, + Password: query.FieldIdentifier[[]uint8]{ + Field: "Password", + ModelType: userType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: userType, + }, + Username: query.FieldIdentifier[string]{ + Field: "Username", + ModelType: userType, + }, +} + +func (userConditions userConditions) Preload() condition.Condition[models.User] { + return condition.NewPreloadCondition[models.User](userConditions.ID, userConditions.CreatedAt, userConditions.UpdatedAt, userConditions.DeletedAt, userConditions.Username, userConditions.Email, userConditions.Password) +} diff --git a/persistence/models/dto/HTTPError.go b/persistence/models/dto/HTTPError.go index 0e352145..e182eadd 100644 --- a/persistence/models/dto/HTTPError.go +++ b/persistence/models/dto/HTTPError.go @@ -1,9 +1,9 @@ package dto -// Data Transfert Object Package +// Data Transfer Object Package // Describe the HTTP Error payload -type DTOHTTPError struct { +type HTTPError struct { Error string `json:"err"` Message string `json:"msg"` Status string `json:"status"` diff --git a/persistence/models/dto/LoginSuccess.go b/persistence/models/dto/LoginSuccess.go index 3601e975..87492bad 100644 --- a/persistence/models/dto/LoginSuccess.go +++ b/persistence/models/dto/LoginSuccess.go @@ -1,8 +1,8 @@ package dto -// DTOLoginSuccess is a dto returned to the client when the authentication is successful. -type DTOLoginSuccess struct { +// LoginSuccess is a dto returned to the client when the authentication is successful. +type LoginSuccess struct { Email string `json:"email"` ID string `json:"id"` Username string `json:"username"` -} \ No newline at end of file +} diff --git a/persistence/models/dto/ProductInfo.go b/persistence/models/dto/ProductInfo.go deleted file mode 100644 index 988650ea..00000000 --- a/persistence/models/dto/ProductInfo.go +++ /dev/null @@ -1,9 +0,0 @@ -package dto - -// Data Transfert Object Package - -// Describe the Server Info payload -type DTOBadaasServerInfo struct { - Status string `json:"status"` - Version string `json:"version"` -} diff --git a/persistence/pagination/Page.go b/persistence/pagination/Page.go deleted file mode 100644 index 2d76a050..00000000 --- a/persistence/pagination/Page.go +++ /dev/null @@ -1,40 +0,0 @@ -package pagination - -import "github.com/ditrit/badaas/persistence/models" - -// A page hold ressources and data regarding the pagination -type Page[T models.Tabler] struct { - Ressources []*T `json:"ressources"` - // max d'element par page - Limit uint `json:"limit"` - // page courante - Offset uint `json:"offset"` - // total d'element en base - Total uint `json:"total"` - // total de pages - TotalPages uint `json:"totalPages"` - HasNextPage bool `json:"hasNextpage"` - HasPreviousPage bool `json:"hasPreviousPage"` - IsFirstPage bool `json:"isFirstPage"` - IsLastPage bool `json:"isLastPage"` - HasContent bool `json:"hasContent"` -} - -// Create a new page -func NewPage[T models.Tabler](records []*T, offset, size, nbElemTotal uint) *Page[T] { - nbPage := nbElemTotal / size - p := Page[T]{ - Ressources: records, - Limit: size, - Offset: offset, - Total: nbElemTotal, - TotalPages: nbPage, - - HasNextPage: nbElemTotal > (offset+1)*size, - HasPreviousPage: offset >= 1, - IsFirstPage: offset == 0, - IsLastPage: offset == (nbPage - 1), - HasContent: len(records) != 0, - } - return &p -} diff --git a/persistence/pagination/Page_test.go b/persistence/pagination/Page_test.go deleted file mode 100644 index 999e5201..00000000 --- a/persistence/pagination/Page_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package pagination_test - -import ( - "testing" - - "github.com/ditrit/badaas/persistence/pagination" - "github.com/stretchr/testify/assert" -) - -type Whatever struct { - DumbData int -} - -func (Whatever) TableName() string { - return "whatevers" -} - -var ( - // test fixture - ressources = []*Whatever{ - {10}, - {11}, - {12}, - {13}, - {14}, - {15}, - {16}, - {17}, - {18}, - {19}, - } -) - -func TestNewPage(t *testing.T) { - p := pagination.NewPage( - ressources, - 1, // page 1 - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.ElementsMatch(t, ressources, p.Ressources) - assert.Equal(t, uint(10), p.Limit) - assert.Equal(t, uint(1), p.Offset) - assert.Equal(t, uint(5), p.TotalPages) -} - -func TestPageHasNextPageFalse(t *testing.T) { - p := pagination.NewPage( - ressources, - 4, // page 4: last page - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.False(t, p.HasNextPage) -} - -func TestPageHasNextPageTrue(t *testing.T) { - p := pagination.NewPage( - ressources, - 1, // page 1 - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.True(t, p.HasNextPage) -} - -func TestPageIsLastPageFalse(t *testing.T) { - p := pagination.NewPage( - ressources, - 1, // page 1 - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.False(t, p.IsLastPage) -} - -func TestPageIsLastPageTrue(t *testing.T) { - p := pagination.NewPage( - ressources, - 4, // page 4: last page - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.True(t, p.IsLastPage) -} - -func TestPageHasPreviousPageFalse(t *testing.T) { - p := pagination.NewPage( - ressources, - 0, // page 1 - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.False(t, p.HasPreviousPage) -} - -func TestPageHasPreviousPageTrue(t *testing.T) { - p := pagination.NewPage( - ressources, - 1, // page 1 - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.True(t, p.HasPreviousPage) -} - -func TestPageIsFirstPageFalse(t *testing.T) { - p := pagination.NewPage( - ressources, - 1, // page 1 - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.False(t, p.IsFirstPage) -} - -func TestPageIsFirstPageTrue(t *testing.T) { - p := pagination.NewPage( - ressources, - 0, // page 0: first page - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.True(t, p.IsFirstPage) -} - -func TestPageHasContentFalse(t *testing.T) { - p := pagination.NewPage( - []*Whatever{}, // no content - 0, // page 1 - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.False(t, p.HasPreviousPage) -} - -func TestPageHasContentTrue(t *testing.T) { - p := pagination.NewPage( - ressources, - 1, // page 1 - 10, // 10 elems per page - 50, // 50 elem in total - ) - assert.True(t, p.HasContent) -} diff --git a/persistence/pagination/Paginator.go b/persistence/pagination/Paginator.go deleted file mode 100644 index e2811a72..00000000 --- a/persistence/pagination/Paginator.go +++ /dev/null @@ -1,36 +0,0 @@ -package pagination - -// Handle pagination -type Paginator interface { - Offset() uint - Limit() uint -} - -type paginatorImpl struct { - offset uint - limit uint -} - -// Constructor of Paginator -func NewPaginator(page, limit uint) Paginator { - if page == 0 { - page = 1 - } - if limit == 0 { - limit = 1 - } - return &paginatorImpl{ - offset: page, - limit: limit, - } -} - -// Return the page number -func (p *paginatorImpl) Offset() uint { - return p.offset -} - -// Return the max number of records for one page -func (p *paginatorImpl) Limit() uint { - return p.limit -} diff --git a/persistence/pagination/Paginator_test.go b/persistence/pagination/Paginator_test.go deleted file mode 100644 index 8fc58329..00000000 --- a/persistence/pagination/Paginator_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package pagination_test - -import ( - "testing" - - "github.com/ditrit/badaas/persistence/pagination" - "github.com/stretchr/testify/assert" -) - -func TestPaginator(t *testing.T) { - paginator := pagination.NewPaginator(uint(0), uint(12)) - assert.NotNil(t, paginator) - assert.Equal(t, uint(12), paginator.Limit()) - - paginator = pagination.NewPaginator(uint(2), uint(12)) - assert.NotNil(t, paginator) - assert.Equal(t, uint(12), paginator.Limit()) - - paginator = pagination.NewPaginator(uint(2), uint(0)) - assert.NotNil(t, paginator) - assert.Equal(t, uint(1), paginator.Limit()) -} diff --git a/persistence/repository/CRUDRepository.go b/persistence/repository/CRUDRepository.go deleted file mode 100644 index e3c45662..00000000 --- a/persistence/repository/CRUDRepository.go +++ /dev/null @@ -1,20 +0,0 @@ -package repository - -import ( - "github.com/Masterminds/squirrel" - "github.com/ditrit/badaas/httperrors" - "github.com/ditrit/badaas/persistence/models" - "github.com/ditrit/badaas/persistence/pagination" -) - -// Generic CRUD Repository -type CRUDRepository[T models.Tabler, ID any] interface { - Create(*T) httperrors.HTTPError - Delete(*T) httperrors.HTTPError - Save(*T) httperrors.HTTPError - GetByID(ID) (*T, httperrors.HTTPError) - GetAll(SortOption) ([]*T, httperrors.HTTPError) - Count(squirrel.Sqlizer) (uint, httperrors.HTTPError) - Find(squirrel.Sqlizer, pagination.Paginator, SortOption) (*pagination.Page[T], httperrors.HTTPError) - Transaction(fn func(CRUDRepository[T, ID]) (any, error)) (any, httperrors.HTTPError) -} diff --git a/persistence/repository/CRUDRepositoryImpl.go b/persistence/repository/CRUDRepositoryImpl.go deleted file mode 100644 index eca2365f..00000000 --- a/persistence/repository/CRUDRepositoryImpl.go +++ /dev/null @@ -1,240 +0,0 @@ -package repository - -import ( - "fmt" - "net/http" - - "github.com/Masterminds/squirrel" - "github.com/ditrit/badaas/configuration" - "github.com/ditrit/badaas/httperrors" - "github.com/ditrit/badaas/persistence/gormdatabase" - "github.com/ditrit/badaas/persistence/models" - "github.com/ditrit/badaas/persistence/pagination" - "go.uber.org/zap" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -// Return a database error -func DatabaseError(message string, golangError error) httperrors.HTTPError { - return httperrors.NewInternalServerError( - "database error", - message, - golangError, - ) -} - -// Implementation of the Generic CRUD Repository -type CRUDRepositoryImpl[T models.Tabler, ID any] struct { - CRUDRepository[T, ID] - gormDatabase *gorm.DB - logger *zap.Logger - paginationConfiguration configuration.PaginationConfiguration -} - -// Contructor of the Generic CRUD Repository -func NewCRUDRepository[T models.Tabler, ID any]( - database *gorm.DB, - logger *zap.Logger, - paginationConfiguration configuration.PaginationConfiguration, -) CRUDRepository[T, ID] { - return &CRUDRepositoryImpl[T, ID]{ - gormDatabase: database, - logger: logger, - paginationConfiguration: paginationConfiguration, - } -} - -// Run the function passed as parameter, if it returns the error and rollback the transaction. -// If no error is returned, it commits the transaction and return the interface{} value. -func (repository *CRUDRepositoryImpl[T, ID]) Transaction(transactionFunction func(CRUDRepository[T, ID]) (any, error)) (any, httperrors.HTTPError) { - transaction := repository.gormDatabase.Begin() - defer func() { - if recoveredError := recover(); recoveredError != nil { - transaction.Rollback() - } - }() - returnValue, err := transactionFunction(&CRUDRepositoryImpl[T, ID]{gormDatabase: transaction}) - if err != nil { - transaction.Rollback() - return nil, DatabaseError("transaction failed", err) - } - err = transaction.Commit().Error - if err != nil { - return nil, DatabaseError("transaction failed to commit", err) - } - return returnValue, nil -} - -// Create an entity of a Model -func (repository *CRUDRepositoryImpl[T, ID]) Create(entity *T) httperrors.HTTPError { - err := repository.gormDatabase.Create(entity).Error - if err != nil { - if gormdatabase.IsDuplicateKeyError(err) { - return httperrors.NewHTTPError( - http.StatusConflict, - fmt.Sprintf("%T already exist in database", entity), - "", - nil, false) - } - return DatabaseError( - fmt.Sprintf("could not create %v in %s", entity, (*entity).TableName()), - err, - ) - - } - return nil -} - -// Delete an entity of a Model -func (repository *CRUDRepositoryImpl[T, ID]) Delete(entity *T) httperrors.HTTPError { - err := repository.gormDatabase.Delete(entity).Error - if err != nil { - return DatabaseError( - fmt.Sprintf("could not delete %v in %s", entity, (*entity).TableName()), - err, - ) - } - return nil -} - -// Save an entity of a Model -func (repository *CRUDRepositoryImpl[T, ID]) Save(entity *T) httperrors.HTTPError { - err := repository.gormDatabase.Save(entity).Error - if err != nil { - return DatabaseError( - fmt.Sprintf("could not save user %v in %s", entity, (*entity).TableName()), - err, - ) - } - return nil -} - -// Get an entity of a Model By ID -func (repository *CRUDRepositoryImpl[T, ID]) GetByID(id ID) (*T, httperrors.HTTPError) { - var entity T - transaction := repository.gormDatabase.First(&entity, "id = ?", id) - if transaction.Error != nil { - return nil, DatabaseError( - fmt.Sprintf("could not get %s by id %v", entity.TableName(), id), - transaction.Error, - ) - } - return &entity, nil -} - -// Get all entities of a Model -func (repository *CRUDRepositoryImpl[T, ID]) GetAll(sortOption SortOption) ([]*T, httperrors.HTTPError) { - var entities []*T - transaction := repository.gormDatabase - if sortOption != nil { - transaction = transaction.Order(buildClauseFromSortOption(sortOption)) - } - transaction.Find(&entities) - if transaction.Error != nil { - var emptyInstanceForError T - return nil, DatabaseError( - fmt.Sprintf("could not get %s", emptyInstanceForError.TableName()), - transaction.Error, - ) - } - return entities, nil -} - -// Build a gorm order clause from a SortOption -func buildClauseFromSortOption(sortOption SortOption) clause.OrderByColumn { - return clause.OrderByColumn{Column: clause.Column{Name: sortOption.Column()}, Desc: sortOption.Desc()} -} - -// Count entities of a models -func (repository *CRUDRepositoryImpl[T, ID]) Count(filters squirrel.Sqlizer) (uint, httperrors.HTTPError) { - whereClause, values, httpError := repository.compileSQL(filters) - if httpError != nil { - return 0, httpError - } - return repository.count(whereClause, values) -} - -// Count the number of record that match the where clause with the provided values on the db -func (repository *CRUDRepositoryImpl[T, ID]) count(whereClause string, values []interface{}) (uint, httperrors.HTTPError) { - var entity *T - var count int64 - transaction := repository.gormDatabase.Model(entity).Where(whereClause, values).Count(&count) - if transaction.Error != nil { - var emptyInstanceForError T - return 0, DatabaseError( - fmt.Sprintf("could not count data from %s with condition %q", emptyInstanceForError.TableName(), whereClause), - transaction.Error, - ) - } - return uint(count), nil -} - -// Find entities of a Model -func (repository *CRUDRepositoryImpl[T, ID]) Find( - filters squirrel.Sqlizer, - page pagination.Paginator, - sortOption SortOption, -) (*pagination.Page[T], httperrors.HTTPError) { - transaction := repository.gormDatabase.Begin() - defer func() { - if recoveredError := recover(); recoveredError != nil { - transaction.Rollback() - - } - }() - var instances []*T - whereClause, values, httpError := repository.compileSQL(filters) - - if httpError != nil { - return nil, httpError - } - if page != nil { - transaction = transaction. - Offset( - int((page.Offset() - 1) * page.Limit()), - ). - Limit( - int(page.Limit()), - ) - } else { - page = pagination.NewPaginator(0, repository.paginationConfiguration.GetMaxElemPerPage()) - } - if sortOption != nil { - transaction = transaction.Order(buildClauseFromSortOption(sortOption)) - } - transaction = transaction.Where(whereClause, values...).Find(&instances) - if transaction.Error != nil { - transaction.Rollback() - var emptyInstanceForError T - return nil, DatabaseError( - fmt.Sprintf("could not get data from %s with condition %q", emptyInstanceForError.TableName(), whereClause), - transaction.Error, - ) - } - // Get Count - nbElem, httpError := repository.count(whereClause, values) - if httpError != nil { - transaction.Rollback() - return nil, httpError - } - err := transaction.Commit().Error - if err != nil { - return nil, DatabaseError( - "transaction failed to commit", err) - } - return pagination.NewPage(instances, page.Offset(), page.Limit(), nbElem), nil -} - -// compile the sql where clause -func (repository *CRUDRepositoryImpl[T, ID]) compileSQL(filters squirrel.Sqlizer) (string, []interface{}, httperrors.HTTPError) { - compiledSQLString, values, err := filters.ToSql() - if err != nil { - return "", []interface{}{}, httperrors.NewInternalServerError( - "sql error", - fmt.Sprintf("Failed to build the sql request (condition=%v)", filters), - err, - ) - } - return compiledSQLString, values, nil -} diff --git a/persistence/repository/CRUDRepositoryImpl_test.go b/persistence/repository/CRUDRepositoryImpl_test.go deleted file mode 100644 index 1dac5dbd..00000000 --- a/persistence/repository/CRUDRepositoryImpl_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package repository - -import ( - "testing" - - "github.com/Masterminds/squirrel" - mocks "github.com/ditrit/badaas/mocks/configuration" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" -) - -func TestDatabaseError(t *testing.T) { - err := DatabaseError("test err", assert.AnError) - require.NotNil(t, err) - assert.True(t, err.Log()) -} - -type dumbModel struct{} - -func (dumbModel) TableName() string { - return "dumb_models" -} - -func TestNewRepository(t *testing.T) { - paginationConfiguration := mocks.NewPaginationConfiguration(t) - dumbModelRepository := NewCRUDRepository[dumbModel, uint](nil, zap.L(), paginationConfiguration) - assert.NotNil(t, dumbModelRepository) -} - -func TestCompileSql_NoError(t *testing.T) { - paginationConfiguration := mocks.NewPaginationConfiguration(t) - dumbModelRepository := &CRUDRepositoryImpl[dumbModel, uint]{ - gormDatabase: nil, - logger: zap.L(), - paginationConfiguration: paginationConfiguration, - } - _, _, err := dumbModelRepository.compileSQL(squirrel.Eq{"name": "qsdqsd"}) - assert.Nil(t, err) -} - -func TestCompileSql_Err(t *testing.T) { - paginationConfiguration := mocks.NewPaginationConfiguration(t) - dumbModelRepository := &CRUDRepositoryImpl[dumbModel, uint]{ - gormDatabase: nil, - logger: zap.L(), - paginationConfiguration: paginationConfiguration, - } - _, _, err := dumbModelRepository.compileSQL(squirrel.GtOrEq{"name": nil}) - - assert.Error(t, err) -} diff --git a/persistence/repository/SortOption.go b/persistence/repository/SortOption.go deleted file mode 100644 index 0fda0c40..00000000 --- a/persistence/repository/SortOption.go +++ /dev/null @@ -1,27 +0,0 @@ -package repository - -type SortOption interface { - Column() string - Desc() bool -} - -// SortOption constructor -func NewSortOption(column string, desc bool) SortOption { - return &sortOption{column, desc} -} - -// Sorting option for the repository -type sortOption struct { - column string - desc bool -} - -// return the column name to sort on -func (sortOption *sortOption) Column() string { - return sortOption.column -} - -// return true for descending sort and false for ascending -func (sortOption *sortOption) Desc() bool { - return sortOption.desc -} diff --git a/persistence/repository/SortOption_test.go b/persistence/repository/SortOption_test.go deleted file mode 100644 index 89eba6c7..00000000 --- a/persistence/repository/SortOption_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package repository_test - -import ( - "testing" - - "github.com/ditrit/badaas/persistence/repository" - "github.com/stretchr/testify/assert" -) - -func TestNewSortOption(t *testing.T) { - sortOption := repository.NewSortOption("a", true) - assert.Equal(t, "a", sortOption.Column()) - assert.True(t, sortOption.Desc()) -} diff --git a/persistence/repository/crud.go b/persistence/repository/crud.go new file mode 100644 index 00000000..fdcda104 --- /dev/null +++ b/persistence/repository/crud.go @@ -0,0 +1,81 @@ +package repository + +import ( + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm" + "github.com/ditrit/badaas/orm/condition" + "github.com/ditrit/badaas/orm/model" +) + +// Generic CRUD Repository +// T can be any model whose identifier attribute is of type ID +type CRUD[T model.Model, ID model.ID] interface { + // Create model "model" inside transaction "tx" + Create(tx *gorm.DB, entity *T) error + + // ----- read ----- + // Get a model by its ID + GetByID(tx *gorm.DB, id ID) (*T, error) + + // Get the list of models that match "conditions" inside transaction "tx" + Find(tx *gorm.DB, conditions ...condition.Condition[T]) ([]*T, error) + + // Get the only one model that match "conditions" inside transaction "tx" + // or returns error if 0 or more than 1 are found. + FindOne(tx *gorm.DB, conditions ...condition.Condition[T]) (*T, error) + + // Save model "model" inside transaction "tx" + Save(tx *gorm.DB, entity *T) error + + // Delete model "model" inside transaction "tx" + Delete(tx *gorm.DB, entity *T) error +} + +// Implementation of the Generic CRUD Repository +type crudImpl[T model.Model, ID model.ID] struct { + CRUD[T, ID] +} + +// Constructor of the Generic CRUD Repository +func NewCRUD[T model.Model, ID model.ID]() CRUD[T, ID] { + return &crudImpl[T, ID]{} +} + +// Create model "model" inside transaction "tx" +func (repository *crudImpl[T, ID]) Create(tx *gorm.DB, model *T) error { + return tx.Create(model).Error +} + +// Delete model "model" inside transaction "tx" +func (repository *crudImpl[T, ID]) Delete(tx *gorm.DB, model *T) error { + return tx.Delete(model).Error +} + +// Save model "model" inside transaction "tx" +func (repository *crudImpl[T, ID]) Save(tx *gorm.DB, model *T) error { + return tx.Save(model).Error +} + +// Get a model by its ID +func (repository *crudImpl[T, ID]) GetByID(tx *gorm.DB, id ID) (*T, error) { + var model T + + err := tx.First(&model, "id = ?", id).Error + if err != nil { + return nil, err + } + + return &model, nil +} + +// Get the list of models that match "conditions" inside transaction "tx" +func (repository *crudImpl[T, ID]) Find(tx *gorm.DB, conditions ...condition.Condition[T]) ([]*T, error) { + return orm.NewQuery[T](tx, conditions...).Find() +} + +// Get the only one model that match "conditions" inside transaction "tx" +// or returns error if 0 or more than 1 are found. +func (repository *crudImpl[T, ID]) FindOne(tx *gorm.DB, conditions ...condition.Condition[T]) (*T, error) { + return orm.NewQuery[T](tx, conditions...).FindOne() +} diff --git a/resources/api.go b/resources/api.go deleted file mode 100644 index ddea375f..00000000 --- a/resources/api.go +++ /dev/null @@ -1,4 +0,0 @@ -package resources - -// Version of Badaas -const Version = "UNRELEASED" diff --git a/router/ModuleFx.go b/router/ModuleFx.go index a9f9d763..cda5eed7 100644 --- a/router/ModuleFx.go +++ b/router/ModuleFx.go @@ -1,19 +1,17 @@ package router import ( - "github.com/ditrit/badaas/router/middlewares" "go.uber.org/fx" + + "github.com/ditrit/badaas/router/middlewares" ) // RouterModule for fx var RouterModule = fx.Module( "router", + fx.Provide(NewRouter), // middlewares fx.Provide(middlewares.NewJSONController), fx.Provide(middlewares.NewMiddlewareLogger), - - fx.Provide(middlewares.NewAuthenticationMiddleware), - - // create router - fx.Provide(SetupRouter), + fx.Invoke(middlewares.AddLoggerMiddleware), ) diff --git a/router/middlewares/middlewareAuthentification.go b/router/middlewares/middlewareAuthentication.go similarity index 89% rename from router/middlewares/middlewareAuthentification.go rename to router/middlewares/middlewareAuthentication.go index aaf8d4b1..c6d8c763 100644 --- a/router/middlewares/middlewareAuthentification.go +++ b/router/middlewares/middlewareAuthentication.go @@ -3,15 +3,14 @@ package middlewares import ( "net/http" + "go.uber.org/zap" + "github.com/ditrit/badaas/httperrors" + "github.com/ditrit/badaas/orm/model" "github.com/ditrit/badaas/services/sessionservice" - "github.com/google/uuid" - "go.uber.org/zap" ) -var ( - NotAuthenticated = httperrors.NewUnauthorizedError("Authentification Error", "not authenticated") -) +var NotAuthenticated = httperrors.NewUnauthorizedError("Authentication Error", "not authenticated") // The authentication middleware type AuthenticationMiddleware interface { @@ -43,7 +42,8 @@ func (authenticationMiddleware *authenticationMiddleware) Handle(next http.Handl NotAuthenticated.Write(response, authenticationMiddleware.logger) return } - extractedUUID, err := uuid.Parse(accessTokenCookie.Value) + + extractedUUID, err := model.ParseUUID(accessTokenCookie.Value) if err != nil { NotAuthenticated.Write(response, authenticationMiddleware.logger) return diff --git a/router/middlewares/middlewareJson.go b/router/middlewares/middlewareJson.go index 1abe0343..b8d0ffd9 100644 --- a/router/middlewares/middlewareJson.go +++ b/router/middlewares/middlewareJson.go @@ -4,8 +4,9 @@ import ( "encoding/json" "net/http" - "github.com/ditrit/badaas/httperrors" "go.uber.org/zap" + + "github.com/ditrit/badaas/httperrors" ) // transform a JSON handler into a standard [http.HandlerFunc] @@ -27,7 +28,8 @@ func NewJSONController(logger *zap.Logger) JSONController { return &jsonControllerImpl{logger} } -// Marshall the response from the JSONHandler and handle HTTPError if needed +// Transforms a JSONHandler into a standard [http.HandlerFunc] +// It marshalls the response from the JSONHandler and handles HTTPError if needed func (controller *jsonControllerImpl) Wrap(handler JSONHandler) func(response http.ResponseWriter, request *http.Request) { return func(response http.ResponseWriter, request *http.Request) { object, herr := handler(response, request) @@ -35,9 +37,11 @@ func (controller *jsonControllerImpl) Wrap(handler JSONHandler) func(response ht herr.Write(response, controller.logger) return } + if object == nil { return } + payload, err := json.Marshal(object) if err != nil { httperrors.NewInternalServerError( @@ -45,9 +49,18 @@ func (controller *jsonControllerImpl) Wrap(handler JSONHandler) func(response ht "Can't marshall the object returned by the JSON handler", nil, ).Write(response, controller.logger) + return } + response.Header().Set("Content-Type", "application/json") - response.Write(payload) + + _, err = response.Write(payload) + if err != nil { + controller.logger.Error( + "Error while writing http response", + zap.String("error", err.Error()), + ) + } } } diff --git a/router/middlewares/middlewareLogger.go b/router/middlewares/middlewareLogger.go index 00f436ad..88e9a7ec 100644 --- a/router/middlewares/middlewareLogger.go +++ b/router/middlewares/middlewareLogger.go @@ -4,12 +4,18 @@ import ( "fmt" "net/http" - "github.com/ditrit/badaas/configuration" + "github.com/gorilla/mux" "github.com/noirbizarre/gonja" "github.com/noirbizarre/gonja/exec" "go.uber.org/zap" + + "github.com/ditrit/badaas/configuration" ) +func AddLoggerMiddleware(router *mux.Router, middlewareLogger MiddlewareLogger) { + router.Use(middlewareLogger.Handle) +} + // Log the requests data type MiddlewareLogger interface { // [github.com/gorilla/mux] compatible middleware function @@ -36,6 +42,7 @@ func NewMiddlewareLogger( if err != nil { return nil, fmt.Errorf("failed to build jinja template from configuration %w", err) } + return &middlewareLoggerImpl{ logger: logger, template: requestLogTemplate, @@ -60,5 +67,6 @@ func getLogMessage(template *exec.Template, r *http.Request) string { "method": r.Method, "url": r.URL.Path, }) + return result } diff --git a/router/middlewares/middlewarelogger_test.go b/router/middlewares/middlewarelogger_test.go index 4d0b2bbe..181816eb 100644 --- a/router/middlewares/middlewarelogger_test.go +++ b/router/middlewares/middlewarelogger_test.go @@ -6,11 +6,12 @@ import ( "net/url" "testing" - configurationmocks "github.com/ditrit/badaas/mocks/configuration" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" + + configurationmocks "github.com/ditrit/badaas/mocks/configuration" ) func TestMiddlewareLogger(t *testing.T) { @@ -18,7 +19,7 @@ func TestMiddlewareLogger(t *testing.T) { observedLogger := zap.New(observedZapCore) req := &http.Request{ - Method: "GET", + Method: http.MethodGet, URL: &url.URL{ Scheme: "http", Host: "localhost", @@ -26,7 +27,8 @@ func TestMiddlewareLogger(t *testing.T) { }, } res := httptest.NewRecorder() - var actuallyRunned bool = false + + actuallyRunned := false // create a handler to use as "next" which will verify the request nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { actuallyRunned = true diff --git a/router/router.go b/router/router.go index 8a3088cc..24039d72 100644 --- a/router/router.go +++ b/router/router.go @@ -1,42 +1,10 @@ package router import ( - "net/http" - - "github.com/ditrit/badaas/controllers" - "github.com/ditrit/badaas/router/middlewares" "github.com/gorilla/mux" ) -// Default router of badaas, initialize all routes. -func SetupRouter( - //middlewares - jsonController middlewares.JSONController, - middlewareLogger middlewares.MiddlewareLogger, - authenticationMiddleware middlewares.AuthenticationMiddleware, - - // controllers - basicAuthentificationController controllers.BasicAuthentificationController, - informationController controllers.InformationController, -) http.Handler { - router := mux.NewRouter() - router.Use(middlewareLogger.Handle) - - router.HandleFunc( - "/info", - jsonController.Wrap(informationController.Info), - ).Methods("GET") - router.HandleFunc( - "/login", - jsonController.Wrap( - basicAuthentificationController.BasicLoginHandler, - ), - ).Methods("POST") - - protected := router.PathPrefix("").Subrouter() - protected.Use(authenticationMiddleware.Handle) - - protected.HandleFunc("/logout", jsonController.Wrap(basicAuthentificationController.Logout)).Methods("GET") - - return router +// Router to use in Badaas server +func NewRouter() *mux.Router { + return mux.NewRouter() } diff --git a/router/router_test.go b/router/router_test.go deleted file mode 100644 index da2f247a..00000000 --- a/router/router_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package router - -import ( - "net/http" - "testing" - - controllersMocks "github.com/ditrit/badaas/mocks/controllers" - middlewaresMocks "github.com/ditrit/badaas/mocks/router/middlewares" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestSetupRouter(t *testing.T) { - jsonController := middlewaresMocks.NewJSONController(t) - middlewareLogger := middlewaresMocks.NewMiddlewareLogger(t) - authenticationMiddleware := middlewaresMocks.NewAuthenticationMiddleware(t) - - basicController := controllersMocks.NewBasicAuthentificationController(t) - informationController := controllersMocks.NewInformationController(t) - jsonController.On("Wrap", mock.Anything).Return(func(response http.ResponseWriter, request *http.Request) {}) - router := SetupRouter(jsonController, middlewareLogger, authenticationMiddleware, basicController, informationController) - assert.NotNil(t, router) -} diff --git a/router/routes.go b/router/routes.go new file mode 100644 index 00000000..68d9fe38 --- /dev/null +++ b/router/routes.go @@ -0,0 +1,43 @@ +package router + +import ( + "github.com/gorilla/mux" + + "github.com/ditrit/badaas/controllers" + "github.com/ditrit/badaas/router/middlewares" +) + +func AddInfoRoutes( + router *mux.Router, + jsonController middlewares.JSONController, + infoController controllers.InformationController, +) { + router.HandleFunc( + "/info", + jsonController.Wrap(infoController.Info), + ).Methods("GET") +} + +// Adds to the "router" the routes for handling authentication: +// /login +// /logout +// And creates a very first user +func AddAuthRoutes( + router *mux.Router, + authenticationMiddleware middlewares.AuthenticationMiddleware, + basicAuthenticationController controllers.BasicAuthenticationController, + jsonController middlewares.JSONController, +) { + router.HandleFunc( + "/login", + jsonController.Wrap(basicAuthenticationController.BasicLoginHandler), + ).Methods("POST") + + protected := router.PathPrefix("").Subrouter() + protected.Use(authenticationMiddleware.Handle) + + protected.HandleFunc( + "/logout", + jsonController.Wrap(basicAuthenticationController.Logout), + ).Methods("GET") +} diff --git a/router/routes_test.go b/router/routes_test.go new file mode 100644 index 00000000..31dbe177 --- /dev/null +++ b/router/routes_test.go @@ -0,0 +1,72 @@ +package router + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" + + "github.com/ditrit/badaas/controllers" + mockControllers "github.com/ditrit/badaas/mocks/controllers" + mockMiddlewares "github.com/ditrit/badaas/mocks/router/middlewares" + "github.com/ditrit/badaas/router/middlewares" +) + +var logger, _ = zap.NewDevelopment() + +func TestAddInfoRoutes(t *testing.T) { + jsonController := middlewares.NewJSONController(logger) + informationController := controllers.NewInfoController(semver.MustParse("1.0.1")) + + router := NewRouter() + AddInfoRoutes( + router, + jsonController, + informationController, + ) + + response := httptest.NewRecorder() + request := httptest.NewRequest( + http.MethodGet, + "/info", + nil, + ) + + router.ServeHTTP(response, request) + assert.Equal(t, response.Code, http.StatusOK) + assert.Equal(t, response.Body.String(), "{\"status\":\"OK\",\"version\":\"1.0.1\"}") +} + +func TestAddLoginRoutes(t *testing.T) { + jsonController := middlewares.NewJSONController(logger) + + basicAuthenticationController := mockControllers.NewBasicAuthenticationController(t) + basicAuthenticationController. + On("BasicLoginHandler", mock.Anything, mock.Anything). + Return(map[string]string{"login": "called"}, nil) + + authenticationMiddleware := mockMiddlewares.NewAuthenticationMiddleware(t) + + router := NewRouter() + AddAuthRoutes( + router, + authenticationMiddleware, + basicAuthenticationController, + jsonController, + ) + + response := httptest.NewRecorder() + request := httptest.NewRequest( + http.MethodPost, + "/login", + nil, + ) + + router.ServeHTTP(response, request) + assert.Equal(t, response.Code, http.StatusOK) + assert.Equal(t, response.Body.String(), "{\"login\":\"called\"}") +} diff --git a/scripts/e2e/api/Dockerfile b/scripts/e2e/api/Dockerfile deleted file mode 100644 index 49bdc272..00000000 --- a/scripts/e2e/api/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -# builder image -FROM golang:1.19-alpine as builder -RUN apk add build-base -WORKDIR /app -COPY . . -RUN CGO_ENABLED=1 go build --race -a -o badaas . - -FROM alpine:3.16.2 -ENV BADAAS_PORT=8000 -COPY --from=builder /app/badaas . -COPY ./scripts/e2e/api/badaas.yml . -ENTRYPOINT [ "./badaas" ] diff --git a/scripts/e2e/db/Dockerfile b/scripts/e2e/db/Dockerfile deleted file mode 100644 index 6f892635..00000000 --- a/scripts/e2e/db/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM cockroachdb/cockroach:latest - -LABEL maintainer="tjveil@gmail.com" - -ADD init.sh /cockroach/ -RUN chmod a+x /cockroach/init.sh - -ADD logs.yaml /cockroach/ - -WORKDIR /cockroach/ - -EXPOSE 8080 -EXPOSE 26257 - -ENTRYPOINT ["/cockroach/init.sh"] \ No newline at end of file diff --git a/scripts/e2e/db/init.sh b/scripts/e2e/db/init.sh deleted file mode 100644 index 7231f8bf..00000000 --- a/scripts/e2e/db/init.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh -echo "******************************* Listing Env Variables..." -printenv -echo "******************************* starting single cockroach node..." - -./cockroach start-single-node --insecure --log-config-file=logs.yaml --background - - -echo "******************************* Creating user" -# cockroach user set ${COCKROACH_USER} --password 1234 --echo-sql -# cockroach user ls - -echo "******************************* Init database" -echo "******************************* |=> Creating init.sql" - -cat > init.sql < Applying init.sql" - -./cockroach sql --insecure --file init.sql - -echo "******************************* To the moon" - -cd /cockroach/cockroach-data/logs -tail -f cockroach.log \ No newline at end of file diff --git a/scripts/e2e/docker-compose.yml b/scripts/e2e/docker-compose.yml deleted file mode 100644 index a6b936a3..00000000 --- a/scripts/e2e/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -# DEVELOPMENT ONLY, DO NOT USE FOR PRODUCTION -version: '3.5' - -services: - db: - build: db/. - ports: - - "26257:26257" - - "8080:8080" # Web based dashboard - environment: - - COCKROACH_USER=root - - COCKROACH_DB=badaas_db - - api: - build: - context: ./../.. - dockerfile: ./scripts/e2e/api/Dockerfile - ports: - - "8000:8000" - restart: always - # environment: - # - BADAAS_PORT=8000 - # - BADAAS_MAX_TIMOUT= 15 # in seconds - depends_on: - - db diff --git a/server.go b/server.go new file mode 100644 index 00000000..f1272b63 --- /dev/null +++ b/server.go @@ -0,0 +1,75 @@ +package badaas + +// This file holds functions needed by the badaas rootCommand, +// those functions help in creating the http.Server. + +import ( + "context" + "net" + "net/http" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "go.uber.org/fx" + "go.uber.org/zap" + + "github.com/ditrit/badaas/configuration" +) + +// Create the server from the configuration holder and the http handler +func createServer(handler http.Handler, httpServerConfig configuration.HTTPServerConfiguration) *http.Server { + timeout := httpServerConfig.GetMaxTimeout() + + return &http.Server{ + Handler: handler, + Addr: httpServerConfig.GetAddr(), + + WriteTimeout: timeout, + ReadTimeout: timeout, + } +} + +func newHTTPServer( + lc fx.Lifecycle, + logger *zap.Logger, + router *mux.Router, + httpServerConfig configuration.HTTPServerConfiguration, +) *http.Server { + handler := handlers.CORS( + handlers.AllowedMethods([]string{"GET", "POST", "DELETE", "PUT", "OPTIONS"}), + handlers.AllowedHeaders([]string{ + "Accept", "Content-Type", "Content-Length", + "Accept-Encoding", "X-CSRF-Token", "Authorization", + "Access-Control-Request-Headers", "Access-Control-Request-Method", + "Connection", "Host", "Origin", "User-Agent", "Referer", + "Cache-Control", "X-header", + }), + handlers.AllowedOrigins([]string{"*"}), + handlers.AllowCredentials(), + handlers.MaxAge(0), + )(router) + + srv := createServer(handler, httpServerConfig) + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + ln, err := net.Listen("tcp", srv.Addr) + if err != nil { + return err + } + logger.Sugar().Infof("Ready to serve at %s", srv.Addr) + go func() { + _ = srv.Serve(ln) + }() + + return nil + }, + OnStop: func(ctx context.Context) error { + // Flush the logger + _ = logger.Sync() + return srv.Shutdown(ctx) + }, + }) + + return srv +} diff --git a/server_test.go b/server_test.go new file mode 100644 index 00000000..cb231be3 --- /dev/null +++ b/server_test.go @@ -0,0 +1,19 @@ +package badaas + +// This files holds the tests for the server.go file. + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ditrit/badaas/configuration" +) + +func TestCreateServer(t *testing.T) { + handl := http.NewServeMux() + + srv := createServer(handl, configuration.NewHTTPServerConfiguration()) + assert.NotNil(t, srv) +} diff --git a/services/ModuleFx.go b/services/ModuleFx.go new file mode 100644 index 00000000..ebabe8d4 --- /dev/null +++ b/services/ModuleFx.go @@ -0,0 +1,34 @@ +package services + +import ( + "go.uber.org/fx" + + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/persistence/gormfx" + "github.com/ditrit/badaas/persistence/models" + "github.com/ditrit/badaas/persistence/repository" + "github.com/ditrit/badaas/services/sessionservice" + "github.com/ditrit/badaas/services/userservice" +) + +var AuthServiceModule = fx.Module( + "authService", + // models + fx.Provide(getAuthModels), + // repositories + fx.Provide(repository.NewCRUD[models.Session, model.UUID]), + fx.Provide(repository.NewCRUD[models.User, model.UUID]), + + // services + fx.Provide(userservice.NewUserService), + fx.Provide(sessionservice.NewSessionService), +) + +func getAuthModels() gormfx.GetModelsResult { + return gormfx.GetModelsResult{ + Models: []any{ + models.Session{}, + models.User{}, + }, + } +} diff --git a/services/auth/protocols/basicauth/basic.go b/services/auth/protocols/basicauth/basic.go index ac2e944d..3c202048 100644 --- a/services/auth/protocols/basicauth/basic.go +++ b/services/auth/protocols/basicauth/basic.go @@ -14,6 +14,7 @@ func SaltAndHashPassword(password string) []byte { []byte(password), cost, ) + return bytes } diff --git a/services/auth/protocols/basicauth/basic_test.go b/services/auth/protocols/basicauth/basic_test.go index c788a018..52e3f6e5 100644 --- a/services/auth/protocols/basicauth/basic_test.go +++ b/services/auth/protocols/basicauth/basic_test.go @@ -3,8 +3,9 @@ package basicauth_test import ( "testing" - "github.com/ditrit/badaas/services/auth/protocols/basicauth" "github.com/stretchr/testify/assert" + + "github.com/ditrit/badaas/services/auth/protocols/basicauth" ) func TestSaltAndHashPassword(t *testing.T) { @@ -18,5 +19,4 @@ func TestCheckUserPassword(t *testing.T) { hash := basicauth.SaltAndHashPassword(password) assert.True(t, basicauth.CheckUserPassword(hash, password), "the password and it's hash should match") assert.False(t, basicauth.CheckUserPassword(hash, "wrong password"), "the password and it's hash should match") - } diff --git a/services/sessionservice/session.go b/services/sessionservice/session.go index d4368319..5de02ecd 100644 --- a/services/sessionservice/session.go +++ b/services/sessionservice/session.go @@ -2,17 +2,17 @@ package sessionservice import ( "fmt" - "net/http" "sync" "time" - "github.com/Masterminds/squirrel" + "go.uber.org/zap" + "gorm.io/gorm" + "github.com/ditrit/badaas/configuration" "github.com/ditrit/badaas/httperrors" + "github.com/ditrit/badaas/orm/model" "github.com/ditrit/badaas/persistence/models" "github.com/ditrit/badaas/persistence/repository" - "github.com/google/uuid" - "go.uber.org/zap" ) // Errors @@ -25,10 +25,11 @@ var ( // SessionService handle sessions type SessionService interface { - IsValid(sessionUUID uuid.UUID) (bool, *SessionClaims) - RollSession(uuid.UUID) httperrors.HTTPError - LogUserIn(user *models.User, response http.ResponseWriter) httperrors.HTTPError - LogUserOut(sessionClaims *SessionClaims, response http.ResponseWriter) httperrors.HTTPError + IsValid(sessionUUID model.UUID) (bool, *SessionClaims) + // TODO services should not work with httperrors + RollSession(model.UUID) httperrors.HTTPError + LogUserIn(user *models.User) (*models.Session, error) + LogUserOut(sessionClaims *SessionClaims) httperrors.HTTPError } // Check interface compliance @@ -36,82 +37,86 @@ var _ SessionService = (*sessionServiceImpl)(nil) // The SessionService concrete interface type sessionServiceImpl struct { - sessionRepository repository.CRUDRepository[models.Session, uuid.UUID] - cache map[uuid.UUID]*models.Session + sessionRepository repository.CRUD[models.Session, model.UUID] + cache map[model.UUID]*models.Session mutex sync.Mutex logger *zap.Logger sessionConfiguration configuration.SessionConfiguration + db *gorm.DB } // The SessionService constructor func NewSessionService( logger *zap.Logger, - sessionRepository repository.CRUDRepository[models.Session, uuid.UUID], + sessionRepository repository.CRUD[models.Session, model.UUID], sessionConfiguration configuration.SessionConfiguration, + db *gorm.DB, ) SessionService { sessionService := &sessionServiceImpl{ - cache: make(map[uuid.UUID]*models.Session), + cache: make(map[model.UUID]*models.Session), logger: logger, sessionRepository: sessionRepository, sessionConfiguration: sessionConfiguration, + db: db, } sessionService.init() - return sessionService -} -// Create a new session -func newSession(userID uuid.UUID, sessionDuration time.Duration) *models.Session { - return &models.Session{ - UserID: userID, - ExpiresAt: time.Now().Add(sessionDuration), - } + return sessionService } // Return true if the session exists and is still valid. // A instance of SessionClaims is returned to be added to the request context if the conditions previously mentioned are met. -func (sessionService *sessionServiceImpl) IsValid(sessionUUID uuid.UUID) (bool, *SessionClaims) { +func (sessionService *sessionServiceImpl) IsValid(sessionUUID model.UUID) (bool, *SessionClaims) { sessionInstance := sessionService.get(sessionUUID) if sessionInstance == nil { return false, nil } + return true, makeSessionClaims(sessionInstance) } // Get a session from cache // return nil if not found -func (sessionService *sessionServiceImpl) get(sessionUUID uuid.UUID) *models.Session { +func (sessionService *sessionServiceImpl) get(sessionUUID model.UUID) *models.Session { sessionService.mutex.Lock() defer sessionService.mutex.Unlock() + session, ok := sessionService.cache[sessionUUID] if ok { return session } - sessionsFoundWithUUID, databaseError := sessionService.sessionRepository.Find(squirrel.Eq{"uuid": sessionUUID.String()}, nil, nil) - if databaseError != nil { + + session, err := sessionService.sessionRepository.GetByID( + sessionService.db, + sessionUUID, + ) + if err != nil { return nil } - if !sessionsFoundWithUUID.HasContent { - return nil // no sessions found in database - } - return sessionsFoundWithUUID.Ressources[0] + + return session } // Add a session to the cache -func (sessionService *sessionServiceImpl) add(session *models.Session) httperrors.HTTPError { +func (sessionService *sessionServiceImpl) add(session *models.Session) error { sessionService.mutex.Lock() defer sessionService.mutex.Unlock() - herr := sessionService.sessionRepository.Create(session) - if herr != nil { - return herr + + err := sessionService.sessionRepository.Create(sessionService.db, session) + if err != nil { + return err } + sessionService.cache[session.ID] = session sessionService.logger.Debug("Added session", zap.String("uuid", session.ID.String())) + return nil } // Initialize the session service -func (sessionService *sessionServiceImpl) init() error { - sessionService.cache = make(map[uuid.UUID]*models.Session) +func (sessionService *sessionServiceImpl) init() { + sessionService.cache = make(map[model.UUID]*models.Session) + go func() { for { sessionService.removeExpired() @@ -121,21 +126,23 @@ func (sessionService *sessionServiceImpl) init() error { ) } }() - return nil } // Get all sessions and save them in cache func (sessionService *sessionServiceImpl) pullFromDB() { sessionService.mutex.Lock() defer sessionService.mutex.Unlock() - sessionsFromDatabase, err := sessionService.sessionRepository.GetAll(nil) + + sessionsFromDatabase, err := sessionService.sessionRepository.Find(sessionService.db) if err != nil { panic(err) } - newSessionCache := make(map[uuid.UUID]*models.Session) + + newSessionCache := make(map[model.UUID]*models.Session) for _, sessionFromDatabase := range sessionsFromDatabase { newSessionCache[sessionFromDatabase.ID] = sessionFromDatabase } + sessionService.cache = newSessionCache sessionService.logger.Debug( "Pulled sessions from DB", @@ -147,11 +154,13 @@ func (sessionService *sessionServiceImpl) pullFromDB() { func (sessionService *sessionServiceImpl) removeExpired() { sessionService.mutex.Lock() defer sessionService.mutex.Unlock() + var i int + for sessionUUID, session := range sessionService.cache { if session.IsExpired() { // Delete the session in the database - err := sessionService.sessionRepository.Delete(session) + err := sessionService.sessionRepository.Delete(sessionService.db, session) if err != nil { panic(err) } @@ -162,6 +171,7 @@ func (sessionService *sessionServiceImpl) removeExpired() { i++ } } + sessionService.logger.Debug( "Removed expired session", zap.Int("expiredSessionCount", i), @@ -172,8 +182,10 @@ func (sessionService *sessionServiceImpl) removeExpired() { func (sessionService *sessionServiceImpl) delete(session *models.Session) httperrors.HTTPError { sessionService.mutex.Lock() defer sessionService.mutex.Unlock() + sessionUUID := session.ID - err := sessionService.sessionRepository.Delete(session) + + err := sessionService.sessionRepository.Delete(sessionService.db, session) if err != nil { return httperrors.NewInternalServerError( "session error", @@ -181,77 +193,70 @@ func (sessionService *sessionServiceImpl) delete(session *models.Session) httper err, ) } + delete(sessionService.cache, sessionUUID) + return nil } // Roll a session. If the session is close to expiration, extend its duration. -func (sessionService *sessionServiceImpl) RollSession(sessionUUID uuid.UUID) httperrors.HTTPError { +func (sessionService *sessionServiceImpl) RollSession(sessionUUID model.UUID) httperrors.HTTPError { rollInterval := sessionService.sessionConfiguration.GetRollDuration() sessionDuration := sessionService.sessionConfiguration.GetSessionDuration() + session := sessionService.get(sessionUUID) if session == nil { // no session to roll, no error return nil } + if session.IsExpired() { return HERRSessionExpired } + if session.CanBeRolled(rollInterval) { sessionService.mutex.Lock() defer sessionService.mutex.Unlock() + session.ExpiresAt = session.ExpiresAt.Add(sessionDuration) - herr := sessionService.sessionRepository.Save(session) - if herr != nil { - return herr + + err := sessionService.sessionRepository.Save(sessionService.db, session) + if err != nil { + return httperrors.NewDBError(err) } + sessionService.logger.Warn("Rolled session", zap.String("userID", session.UserID.String()), zap.String("sessionID", session.ID.String())) } + return nil } // Log in a user -func (sessionService *sessionServiceImpl) LogUserIn(user *models.User, response http.ResponseWriter) httperrors.HTTPError { +func (sessionService *sessionServiceImpl) LogUserIn(user *models.User) (*models.Session, error) { sessionDuration := sessionService.sessionConfiguration.GetSessionDuration() - session := newSession(user.ID, sessionDuration) + session := models.NewSession(user.ID, sessionDuration) + err := sessionService.add(session) if err != nil { - return err + return nil, err } - CreateAndSetAccessTokenCookie(response, session.ID.String()) - return nil + + return session, nil } // Log out a user. -func (sessionService *sessionServiceImpl) LogUserOut(sessionClaims *SessionClaims, response http.ResponseWriter) httperrors.HTTPError { +func (sessionService *sessionServiceImpl) LogUserOut(sessionClaims *SessionClaims) httperrors.HTTPError { session := sessionService.get(sessionClaims.SessionUUID) if session == nil { - return httperrors.NewUnauthorizedError("Authentification Error", "not authenticated") + return httperrors.NewUnauthorizedError("Authentication Error", "not authenticated") } + err := sessionService.delete(session) if err != nil { return err } - CreateAndSetAccessTokenCookie(response, "") - return nil -} -// Create an access token and send it in a cookie -func CreateAndSetAccessTokenCookie(w http.ResponseWriter, sessionUUID string) { - accessToken := &http.Cookie{ - Name: "access_token", - Path: "/", - Value: sessionUUID, - HttpOnly: true, - SameSite: http.SameSiteNoneMode, // change to http.SameSiteStrictMode in prod - Secure: false, // change to true in prod - Expires: time.Now().Add(48 * time.Hour), - } - err := accessToken.Valid() - if err != nil { - panic(err) - } - http.SetCookie(w, accessToken) + return nil } diff --git a/services/sessionservice/session_test.go b/services/sessionservice/session_test.go index 422b831f..3b538ecd 100644 --- a/services/sessionservice/session_test.go +++ b/services/sessionservice/session_test.go @@ -1,83 +1,83 @@ package sessionservice import ( - "net/http/httptest" + "errors" "testing" "time" - "github.com/Masterminds/squirrel" - "github.com/ditrit/badaas/httperrors" - configurationmocks "github.com/ditrit/badaas/mocks/configuration" - repositorymocks "github.com/ditrit/badaas/mocks/persistence/repository" - "github.com/ditrit/badaas/persistence/models" - "github.com/ditrit/badaas/persistence/pagination" - "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" + "gorm.io/gorm" + + "github.com/ditrit/badaas/httperrors" + configurationMocks "github.com/ditrit/badaas/mocks/configuration" + repositoryMocks "github.com/ditrit/badaas/mocks/persistence/repository" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/persistence/models" ) -func TestNewSession(t *testing.T) { - sessionInstance := newSession(uuid.Nil, time.Second) - assert.NotNil(t, sessionInstance) - assert.Equal(t, uuid.Nil, sessionInstance.UserID) -} - -func TestLogInUser(t *testing.T) { - sessionRepositoryMock, service, logs, sessionConfigurationMock := setupTest(t) - sessionRepositoryMock.On("Create", mock.Anything).Return(nil) - sessionConfigurationMock.On("GetSessionDuration").Return(time.Minute) - response := httptest.NewRecorder() - user := &models.User{ - Username: "bob", - Email: "bob@email.com", - } - err := service.LogUserIn(user, response) - require.NoError(t, err) - assert.Len(t, service.cache, 1) - assert.Equal(t, 1, logs.Len()) - log := logs.All()[0] - assert.Equal(t, "Added session", log.Message) - require.Len(t, log.Context, 1) -} +var gormDB *gorm.DB // make values for test func setupTest( t *testing.T, ) ( - *repositorymocks.CRUDRepository[models.Session, uuid.UUID], + *repositoryMocks.CRUD[models.Session, model.UUID], *sessionServiceImpl, *observer.ObservedLogs, - *configurationmocks.SessionConfiguration, + *configurationMocks.SessionConfiguration, ) { core, logs := observer.New(zap.DebugLevel) logger := zap.New(core) - sessionRepositoryMock := repositorymocks.NewCRUDRepository[models.Session, uuid.UUID](t) - sessionConfiguration := configurationmocks.NewSessionConfiguration(t) + sessionRepositoryMock := repositoryMocks.NewCRUD[models.Session, model.UUID](t) + sessionConfiguration := configurationMocks.NewSessionConfiguration(t) service := &sessionServiceImpl{ sessionRepository: sessionRepositoryMock, logger: logger, - cache: make(map[uuid.UUID]*models.Session), + cache: make(map[model.UUID]*models.Session), sessionConfiguration: sessionConfiguration, + db: gormDB, } return sessionRepositoryMock, service, logs, sessionConfiguration } +func TestLogInUser(t *testing.T) { + sessionRepositoryMock, service, logs, sessionConfigurationMock := setupTest(t) + sessionRepositoryMock.On("Create", gormDB, mock.Anything).Return(nil) + + sessionConfigurationMock.On("GetSessionDuration").Return(time.Minute) + + user := &models.User{ + Username: "bob", + Email: "bob@email.com", + } + _, err := service.LogUserIn(user) + require.NoError(t, err) + assert.Len(t, service.cache, 1) + assert.Equal(t, 1, logs.Len()) + log := logs.All()[0] + assert.Equal(t, "Added session", log.Message) + require.Len(t, log.Context, 1) +} + func TestLogInUserDbError(t *testing.T) { sessionRepositoryMock, service, logs, sessionConfigurationMock := setupTest(t) - sessionRepositoryMock.On("Create", mock.Anything).Return(httperrors.NewInternalServerError("db err", "nil", nil)) + sessionRepositoryMock. + On("Create", gormDB, mock.Anything). + Return(errors.New("db err")) + sessionConfigurationMock.On("GetSessionDuration").Return(time.Minute) - response := httptest.NewRecorder() user := &models.User{ Username: "bob", Email: "bob@email.com", } - err := service.LogUserIn(user, response) + _, err := service.LogUserIn(user) require.Error(t, err) assert.Len(t, service.cache, 0) assert.Equal(t, 0, logs.Len()) @@ -85,23 +85,24 @@ func TestLogInUserDbError(t *testing.T) { func TestIsValid(t *testing.T) { sessionRepositoryMock, service, _, _ := setupTest(t) - sessionRepositoryMock.On("Create", mock.Anything).Return(nil) - uuidSample := uuid.New() + sessionRepositoryMock.On("Create", gormDB, mock.Anything).Return(nil) + + uuidSample := model.NewUUID() session := &models.Session{ - BaseModel: models.BaseModel{ + UUIDModel: model.UUIDModel{ ID: uuidSample, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: time.Now().Add(time.Hour), } err := service.add(session) require.NoError(t, err) assert.Len(t, service.cache, 1) - assert.Equal(t, uuid.Nil, service.cache[uuidSample].UserID) + assert.Equal(t, model.NilUUID, service.cache[uuidSample].UserID) isValid, claims := service.IsValid(uuidSample) require.True(t, isValid) assert.Equal(t, *claims, SessionClaims{ - UserID: uuid.Nil, + UserID: model.NilUUID, SessionUUID: uuidSample, }) } @@ -109,46 +110,49 @@ func TestIsValid(t *testing.T) { func TestIsValid_SessionNotFound(t *testing.T) { sessionRepositoryMock, service, _, _ := setupTest(t) sessionRepositoryMock. - On("Find", mock.Anything, mock.Anything, mock.Anything). - Return(pagination.NewPage([]*models.Session{}, 0, 125, 1236), nil) - uuidSample := uuid.New() + On("GetByID", gormDB, mock.Anything). + Return(nil, errors.New("not-found")) + + uuidSample := model.NewUUID() isValid, _ := service.IsValid(uuidSample) require.False(t, isValid) - // } func TestLogOutUser(t *testing.T) { sessionRepositoryMock, service, _, _ := setupTest(t) - sessionRepositoryMock.On("Delete", mock.Anything).Return(nil) - response := httptest.NewRecorder() - uuidSample := uuid.New() + sessionRepositoryMock.On("Delete", gormDB, mock.Anything).Return(nil) + + uuidSample := model.NewUUID() session := &models.Session{ - BaseModel: models.BaseModel{ + UUIDModel: model.UUIDModel{ ID: uuidSample, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: time.Now().Add(time.Hour), } service.cache[uuidSample] = session - err := service.LogUserOut(makeSessionClaims(session), response) + err := service.LogUserOut(makeSessionClaims(session)) require.NoError(t, err) assert.Len(t, service.cache, 0) } func TestLogOutUserDbError(t *testing.T) { sessionRepositoryMock, service, _, _ := setupTest(t) - sessionRepositoryMock.On("Delete", mock.Anything).Return(httperrors.NewInternalServerError("db errors", "oh we failed to delete the session", nil)) - response := httptest.NewRecorder() - uuidSample := uuid.New() + sessionRepositoryMock. + On("Delete", gormDB, mock.Anything). + Return(errors.New("db errors")) + + uuidSample := model.NewUUID() + session := &models.Session{ - BaseModel: models.BaseModel{ + UUIDModel: model.UUIDModel{ ID: uuidSample, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: time.Now().Add(time.Hour), } service.cache[uuidSample] = session - err := service.LogUserOut(makeSessionClaims(session), response) + err := service.LogUserOut(makeSessionClaims(session)) require.Error(t, err) assert.Len(t, service.cache, 1) } @@ -156,38 +160,40 @@ func TestLogOutUserDbError(t *testing.T) { func TestLogOutUser_SessionNotFound(t *testing.T) { sessionRepositoryMock, service, _, _ := setupTest(t) sessionRepositoryMock. - On("Find", mock.Anything, nil, nil). - Return(nil, httperrors.NewInternalServerError("db errors", "oh we failed to delete the session", nil)) - response := httptest.NewRecorder() - uuidSample := uuid.New() + On("GetByID", gormDB, mock.Anything). + Return(nil, errors.New("not-found")) + + uuidSample := model.NewUUID() session := &models.Session{ - BaseModel: models.BaseModel{ - ID: uuid.Nil, + UUIDModel: model.UUIDModel{ + ID: model.NilUUID, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: time.Now().Add(time.Hour), } service.cache[uuidSample] = session sessionClaims := makeSessionClaims(session) - sessionClaims.SessionUUID = uuid.Nil - err := service.LogUserOut(sessionClaims, response) + sessionClaims.SessionUUID = model.NilUUID + err := service.LogUserOut(sessionClaims) require.Error(t, err) assert.Len(t, service.cache, 1) } func TestRollSession(t *testing.T) { sessionRepositoryMock, service, _, sessionConfigurationMock := setupTest(t) - sessionRepositoryMock.On("Save", mock.Anything).Return(nil) + sessionRepositoryMock.On("Save", gormDB, mock.Anything).Return(nil) + sessionDuration := time.Minute sessionConfigurationMock.On("GetSessionDuration").Return(sessionDuration) sessionConfigurationMock.On("GetRollDuration").Return(sessionDuration / 4) - uuidSample := uuid.New() + + uuidSample := model.NewUUID() originalExpirationTime := time.Now().Add(sessionDuration / 5) session := &models.Session{ - BaseModel: models.BaseModel{ - ID: uuid.Nil, + UUIDModel: model.UUIDModel{ + ID: model.NilUUID, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: originalExpirationTime, } service.cache[uuidSample] = session @@ -201,13 +207,14 @@ func TestRollSession_Expired(t *testing.T) { sessionDuration := time.Minute sessionConfigurationMock.On("GetSessionDuration").Return(sessionDuration) sessionConfigurationMock.On("GetRollDuration").Return(sessionDuration / 4) - uuidSample := uuid.New() + + uuidSample := model.NewUUID() originalExpirationTime := time.Now().Add(-time.Hour) session := &models.Session{ - BaseModel: models.BaseModel{ + UUIDModel: model.UUIDModel{ ID: uuidSample, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: originalExpirationTime, } service.cache[uuidSample] = session @@ -221,46 +228,49 @@ func TestRollSession_falseUUID(t *testing.T) { sessionConfigurationMock.On("GetSessionDuration").Return(sessionDuration) sessionConfigurationMock.On("GetRollDuration").Return(sessionDuration / 4) - uuidSample := uuid.New() + uuidSample := model.NewUUID() originalExpirationTime := time.Now().Add(-time.Hour) session := &models.Session{ - BaseModel: models.BaseModel{ - ID: uuid.Nil, + UUIDModel: model.UUIDModel{ + ID: model.NilUUID, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: originalExpirationTime, } service.cache[uuidSample] = session - repoSession.On("Find", mock.Anything, nil, nil).Return(pagination.NewPage([]*models.Session{}, 0, 2, 5), nil) - err := service.RollSession(uuid.New()) + + repoSession. + On("GetByID", gormDB, mock.Anything). + Return(nil, errors.New("not-found")) + + err := service.RollSession(model.NewUUID()) require.NoError(t, err) } func TestRollSession_sessionNotFound(t *testing.T) { sessionRepositoryMock, service, _, sessionConfigurationMock := setupTest(t) sessionRepositoryMock. - On("Find", squirrel.Eq{"uuid": "00000000-0000-0000-0000-000000000000"}, nil, nil). - Return( - pagination.NewPage([]*models.Session{}, 0, 10, 0), nil) + On("GetByID", gormDB, model.NilUUID). + Return(nil, errors.New("not-found")) sessionDuration := time.Minute sessionConfigurationMock.On("GetSessionDuration").Return(sessionDuration) sessionConfigurationMock.On("GetRollDuration").Return(sessionDuration) - err := service.RollSession(uuid.Nil) + err := service.RollSession(model.NilUUID) require.NoError(t, err) } func Test_pullFromDB(t *testing.T) { sessionRepositoryMock, service, logs, _ := setupTest(t) session := &models.Session{ - BaseModel: models.BaseModel{ - ID: uuid.Nil, + UUIDModel: model.UUIDModel{ + ID: model.NilUUID, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: time.Now().Add(time.Hour), } - sessionRepositoryMock.On("GetAll", nil).Return([]*models.Session{session}, nil) + sessionRepositoryMock.On("Find", gormDB).Return([]*models.Session{session}, nil) service.pullFromDB() assert.Len(t, service.cache, 1) @@ -274,23 +284,24 @@ func Test_pullFromDB(t *testing.T) { func Test_pullFromDB_repoError(t *testing.T) { sessionRepositoryMock, service, _, _ := setupTest(t) - sessionRepositoryMock.On("GetAll", nil).Return(nil, httperrors.AnError) - assert.PanicsWithError(t, httperrors.AnError.Error(), func() { service.pullFromDB() }) + sessionRepositoryMock.On("Find", gormDB).Return(nil, httperrors.ErrForTests) + assert.PanicsWithError(t, httperrors.ErrForTests.Error(), func() { service.pullFromDB() }) } func Test_removeExpired(t *testing.T) { sessionRepositoryMock, service, logs, _ := setupTest(t) - uuidSample := uuid.New() + uuidSample := model.NewUUID() session := &models.Session{ - BaseModel: models.BaseModel{ - ID: uuid.Nil, + UUIDModel: model.UUIDModel{ + ID: model.NilUUID, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: time.Now().Add(-time.Hour), } sessionRepositoryMock. - On("Delete", session). + On("Delete", gormDB, session). Return(nil) + service.cache[uuidSample] = session service.removeExpired() @@ -305,17 +316,18 @@ func Test_removeExpired(t *testing.T) { func Test_removeExpired_RepositoryError(t *testing.T) { sessionRepositoryMock, service, _, _ := setupTest(t) - uuidSample := uuid.New() + uuidSample := model.NewUUID() session := &models.Session{ - BaseModel: models.BaseModel{ - ID: uuid.Nil, + UUIDModel: model.UUIDModel{ + ID: model.NilUUID, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: time.Now().Add(-time.Hour), } sessionRepositoryMock. - On("Delete", session). - Return(httperrors.AnError) + On("Delete", gormDB, session). + Return(httperrors.ErrForTests) + service.cache[uuidSample] = session assert.Panics(t, func() { service.removeExpired() }) @@ -323,17 +335,17 @@ func Test_removeExpired_RepositoryError(t *testing.T) { func Test_get(t *testing.T) { sessionRepositoryMock, service, _, _ := setupTest(t) - uuidSample := uuid.New() + uuidSample := model.NewUUID() session := &models.Session{ - BaseModel: models.BaseModel{ - ID: uuid.Nil, + UUIDModel: model.UUIDModel{ + ID: model.NilUUID, }, - UserID: uuid.Nil, + UserID: model.NilUUID, ExpiresAt: time.Now().Add(-time.Hour), } sessionRepositoryMock. - On("Find", mock.Anything, nil, nil). - Return(pagination.NewPage([]*models.Session{session}, 0, 12, 13), nil) + On("GetByID", gormDB, mock.Anything). + Return(session, nil) sessionFound := service.get(uuidSample) assert.Equal(t, sessionFound, session) diff --git a/services/sessionservice/sessionctx.go b/services/sessionservice/sessionctx.go index 6710766a..b1a08eef 100644 --- a/services/sessionservice/sessionctx.go +++ b/services/sessionservice/sessionctx.go @@ -3,14 +3,14 @@ package sessionservice import ( "context" + "github.com/ditrit/badaas/orm/model" "github.com/ditrit/badaas/persistence/models" - "github.com/google/uuid" ) // The session claims passed in the request context type SessionClaims struct { - UserID uuid.UUID - SessionUUID uuid.UUID + UserID model.UUID + SessionUUID model.UUID } // Unique claim key type @@ -41,5 +41,6 @@ func GetSessionClaimsFromContext(ctx context.Context) *SessionClaims { if !ok { panic("could not extract claims from context") } + return claims } diff --git a/services/sessionservice/sessionctx_test.go b/services/sessionservice/sessionctx_test.go index 88b3f954..64b9518f 100644 --- a/services/sessionservice/sessionctx_test.go +++ b/services/sessionservice/sessionctx_test.go @@ -6,17 +6,20 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" + + "github.com/ditrit/badaas/orm/model" ) func TestSessionCtx(t *testing.T) { ctx := context.Background() - sessionClaims := &SessionClaims{uuid.Nil, uuid.New()} + sessionClaims := &SessionClaims{model.NilUUID, model.UUID(uuid.New())} ctx = SetSessionClaimsContext(ctx, sessionClaims) claims := GetSessionClaimsFromContext(ctx) - assert.Equal(t, uuid.Nil, claims.UserID) + assert.Equal(t, model.NilUUID, claims.UserID) } func TestSessionCtxPanic(t *testing.T) { ctx := context.Background() + assert.Panics(t, func() { GetSessionClaimsFromContext(ctx) }) } diff --git a/services/userservice/userservice.go b/services/userservice/userservice.go index 3f3da399..d929da39 100644 --- a/services/userservice/userservice.go +++ b/services/userservice/userservice.go @@ -1,82 +1,93 @@ package userservice import ( + "errors" "fmt" - "github.com/Masterminds/squirrel" - "github.com/ditrit/badaas/httperrors" + "go.uber.org/zap" + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm/model" "github.com/ditrit/badaas/persistence/models" + "github.com/ditrit/badaas/persistence/models/conditions" "github.com/ditrit/badaas/persistence/models/dto" "github.com/ditrit/badaas/persistence/repository" "github.com/ditrit/badaas/services/auth/protocols/basicauth" - validator "github.com/ditrit/badaas/validators" - "github.com/google/uuid" - "go.uber.org/zap" + "github.com/ditrit/badaas/utils/validators" ) // UserService provide functions related to Users type UserService interface { NewUser(username, email, password string) (*models.User, error) - GetUser(dto.UserLoginDTO) (*models.User, httperrors.HTTPError) + GetUser(dto.UserLoginDTO) (*models.User, error) } +var ErrWrongPassword = errors.New("password is incorrect") + // Check interface compliance var _ UserService = (*userServiceImpl)(nil) // The UserService concrete implementation type userServiceImpl struct { - userRepository repository.CRUDRepository[models.User, uuid.UUID] + userRepository repository.CRUD[models.User, model.UUID] logger *zap.Logger + db *gorm.DB } // UserService constructor func NewUserService( logger *zap.Logger, - userRepository repository.CRUDRepository[models.User, uuid.UUID], + userRepository repository.CRUD[models.User, model.UUID], + db *gorm.DB, ) UserService { return &userServiceImpl{ logger: logger, userRepository: userRepository, + db: db, } } // Create a new user func (userService *userServiceImpl) NewUser(username, email, password string) (*models.User, error) { - sanitizedEmail, err := validator.ValidEmail(email) + sanitizedEmail, err := validators.ValidEmail(email) if err != nil { return nil, fmt.Errorf("the provided email is not valid") } + u := &models.User{ Username: username, Email: sanitizedEmail, Password: basicauth.SaltAndHashPassword(password), } - httpError := userService.userRepository.Create(u) - if httpError != nil { - return nil, httpError + + err = userService.userRepository.Create(userService.db, u) + if err != nil { + return nil, err } - userService.logger.Info("Successfully created a new user", - zap.String("email", sanitizedEmail), zap.String("username", username)) + + userService.logger.Info( + "Successfully created a new user", + zap.String("email", sanitizedEmail), + zap.String("username", username), + ) return u, nil } // Get user if the email and password provided are correct, return an error if not. -func (userService *userServiceImpl) GetUser(userLoginDTO dto.UserLoginDTO) (*models.User, httperrors.HTTPError) { - users, herr := userService.userRepository.Find(squirrel.Eq{"email": userLoginDTO.Email}, nil, nil) - if herr != nil { - return nil, herr - } - if !users.HasContent { - return nil, httperrors.NewErrorNotFound("user", - fmt.Sprintf("no user found with email %q", userLoginDTO.Email)) +func (userService *userServiceImpl) GetUser(userLoginDTO dto.UserLoginDTO) (*models.User, error) { + user, err := userService.userRepository.FindOne( + userService.db, + conditions.User.EmailIs().Eq(userLoginDTO.Email), + ) + if err != nil { + return nil, err } - user := users.Ressources[0] - // Check password if !basicauth.CheckUserPassword(user.Password, userLoginDTO.Password) { - return nil, httperrors.NewUnauthorizedError("wrong password", "the provided password is incorrect") + return nil, ErrWrongPassword } + return user, nil } diff --git a/services/userservice/userservice_test.go b/services/userservice/userservice_test.go index df435ed8..172a7ec2 100644 --- a/services/userservice/userservice_test.go +++ b/services/userservice/userservice_test.go @@ -1,31 +1,36 @@ package userservice_test import ( + "errors" "testing" - "github.com/ditrit/badaas/httperrors" - repositorymocks "github.com/ditrit/badaas/mocks/persistence/repository" - "github.com/ditrit/badaas/persistence/models" - "github.com/ditrit/badaas/persistence/models/dto" - "github.com/ditrit/badaas/persistence/pagination" - "github.com/ditrit/badaas/services/userservice" - "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" + "gorm.io/gorm" + + repositoryMocks "github.com/ditrit/badaas/mocks/persistence/repository" + badaasORMErrors "github.com/ditrit/badaas/orm/errors" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/persistence/models" + "github.com/ditrit/badaas/persistence/models/dto" + "github.com/ditrit/badaas/services/userservice" ) +var gormDB *gorm.DB + func TestNewUserService(t *testing.T) { // creating logger observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - userRespositoryMock := repositorymocks.NewCRUDRepository[models.User, uuid.UUID](t) - userRespositoryMock.On("Create", mock.Anything).Return(nil) - userService := userservice.NewUserService(observedLogger, userRespositoryMock) + userRepositoryMock := repositoryMocks.NewCRUD[models.User, model.UUID](t) + userRepositoryMock.On("Create", gormDB, mock.Anything).Return(nil) + + userService := userservice.NewUserService(observedLogger, userRepositoryMock, gormDB) user, err := userService.NewUser("bob", "bob@email.com", "1234") assert.NoError(t, err) assert.NotNil(t, user) @@ -49,13 +54,14 @@ func TestNewUserServiceDatabaseError(t *testing.T) { observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - userRespositoryMock := repositorymocks.NewCRUDRepository[models.User, uuid.UUID](t) - userRespositoryMock.On( - "Create", mock.Anything, + userRepositoryMock := repositoryMocks.NewCRUD[models.User, model.UUID](t) + userRepositoryMock.On( + "Create", gormDB, mock.Anything, ).Return( - httperrors.NewInternalServerError("database error", "test error", nil), + errors.New("database error"), ) - userService := userservice.NewUserService(observedLogger, userRespositoryMock) + + userService := userservice.NewUserService(observedLogger, userRepositoryMock, gormDB) user, err := userService.NewUser("bob", "bob@email.com", "1234") assert.Error(t, err) assert.Nil(t, user) @@ -69,9 +75,9 @@ func TestNewUserServiceEmailNotValid(t *testing.T) { observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - userRespositoryMock := repositorymocks.NewCRUDRepository[models.User, uuid.UUID](t) + userRepositoryMock := repositoryMocks.NewCRUD[models.User, model.UUID](t) - userService := userservice.NewUserService(observedLogger, userRespositoryMock) + userService := userservice.NewUserService(observedLogger, userRepositoryMock, gormDB) user, err := userService.NewUser("bob", "bob@", "1234") assert.Error(t, err) assert.Nil(t, user) @@ -85,20 +91,19 @@ func TestGetUser(t *testing.T) { observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - userRespositoryMock := repositorymocks.NewCRUDRepository[models.User, uuid.UUID](t) - userService := userservice.NewUserService(observedLogger, userRespositoryMock) - userRespositoryMock.On( - "Create", mock.Anything, - ).Return( - nil, - ) + userRepositoryMock := repositoryMocks.NewCRUD[models.User, model.UUID](t) + userService := userservice.NewUserService(observedLogger, userRepositoryMock, gormDB) + userRepositoryMock.On( + "Create", gormDB, mock.Anything, + ).Return(nil) + user, err := userService.NewUser("bob", "bob@email.com", "1234") require.NoError(t, err) - userRespositoryMock.On( - "Find", mock.Anything, nil, nil, + userRepositoryMock.On( + "FindOne", gormDB, mock.Anything, ).Return( - pagination.NewPage([]*models.User{user}, 1, 10, 50), + user, nil, ) @@ -121,13 +126,13 @@ func TestGetUserNoUserFound(t *testing.T) { observedZapCore, _ := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - userRespositoryMock := repositorymocks.NewCRUDRepository[models.User, uuid.UUID](t) - userService := userservice.NewUserService(observedLogger, userRespositoryMock) - userRespositoryMock.On( - "Find", mock.Anything, nil, nil, + userRepositoryMock := repositoryMocks.NewCRUD[models.User, model.UUID](t) + userService := userservice.NewUserService(observedLogger, userRepositoryMock, gormDB) + userRepositoryMock.On( + "FindOne", gormDB, mock.Anything, ).Return( - nil, - httperrors.NewErrorNotFound("user", "user with email bobnotfound@email.com"), + &models.User{}, + badaasORMErrors.ErrObjectNotFound, ) userFound, err := userService.GetUser(dto.UserLoginDTO{Email: "bobnotfound@email.com", Password: "1234"}) @@ -136,53 +141,24 @@ func TestGetUserNoUserFound(t *testing.T) { } // Check what happen if the pass word is not correct -func TestGetUserNotCorrect(t *testing.T) { +func TestGetUserWrongPassword(t *testing.T) { // creating logger observedZapCore, _ := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - userRespositoryMock := repositorymocks.NewCRUDRepository[models.User, uuid.UUID](t) - userRespositoryMock.On( - "Create", mock.Anything, - ).Return( - nil, - ) - userService := userservice.NewUserService(observedLogger, userRespositoryMock) - user, err := userService.NewUser("bob", "bob@email.com", "1234") - - require.NoError(t, err) - userRespositoryMock.On( - "Find", mock.Anything, nil, nil, - ).Return( - pagination.NewPage([]*models.User{user}, 1, 10, 50), - nil, - ) - - userFound, err := userService.GetUser(dto.UserLoginDTO{Email: "bob@email.com", Password: " ../ + +require ( + github.com/Masterminds/semver/v3 v3.1.1 + github.com/cucumber/godog v0.12.5 + github.com/cucumber/messages-go/v16 v16.0.1 + github.com/ditrit/badaas v0.0.0 + github.com/elliotchance/pie/v2 v2.7.0 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.16.0 + go.uber.org/zap v1.24.0 + gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 +) + +require ( + github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/ditrit/verdeter v0.4.0 // indirect + github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gofrs/uuid v4.0.0+incompatible // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/goph/emperror v0.17.2 // indirect + github.com/gorilla/handlers v1.5.1 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-memdb v1.3.3 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/noirbizarre/gonja v0.0.0-20200629003239-4d051fd0be61 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.9.2 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/dig v1.17.0 // indirect + go.uber.org/fx v1.19.3 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.5.2 // indirect + gotest.tools v2.2.0+incompatible // indirect +) diff --git a/test_e2e/go.sum b/test_e2e/go.sum new file mode 100644 index 00000000..ed4a936a --- /dev/null +++ b/test_e2e/go.sum @@ -0,0 +1,750 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bmuller/arrow v0.0.0-20180318014521-b14bfde8dff2/go.mod h1:+voQMVaya0tr8p3W33Qxj/dKOjZNCepW+k8JJvt91gk= +github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +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= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE= +github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw= +github.com/cucumber/godog v0.12.5 h1:FZIy6VCfMbmGHts9qd6UjBMT9abctws/pQYO/ZcwOVs= +github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6Tm9t5pIc= +github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= +github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY= +github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/ditrit/verdeter v0.4.0 h1:DzEOFauuXEGNQYP6OgYtHwEyb3w9riem99u0xE/l7+o= +github.com/ditrit/verdeter v0.4.0/go.mod h1:sKpWuOvYqNabLN4aNXqeBhcWpt7nf0frwqk0B5M6ax0= +github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= +github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/goph/emperror v0.17.1/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.0/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g= +github.com/hashicorp/go-memdb v1.3.3 h1:oGfEWrFuxtIUF3W2q/Jzt6G85TrMk9ey6XfYLvVe1Wo= +github.com/hashicorp/go-memdb v1.3.3/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +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= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microsoft/go-mssqldb v1.1.0 h1:jsV+tpvcPTbNNKW0o3kiCD69kOHICsfjZ2VcVu2lKYc= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/noirbizarre/gonja v0.0.0-20200629003239-4d051fd0be61 h1:8HaKr2WO2B5XKEFbJE9Z7W8mWC6+dL3jZCw53Dbl0oI= +github.com/noirbizarre/gonja v0.0.0-20200629003239-4d051fd0be61/go.mod h1:WboHq+I9Ck8PwKsVFJNrpiRyngXhquRSTWBGwuSWOrg= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +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.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= +go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= +go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA= +go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +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-20220321173239-a90fa8a75705 h1:ba9YlqfDGTTQ5aZ2fwOoQ1hf32QySyQkR6ODGDzHlnE= +golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +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= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +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= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +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= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +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= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= +gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= +gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= +gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc= +gorm.io/driver/sqlserver v1.5.1 h1:wpyW/pR26U94uaujltiFGXY7fd2Jw5hC9PB1ZF/Y5s4= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/test_e2e/http_support_test.go b/test_e2e/http_support_test.go new file mode 100644 index 00000000..dedd9a2e --- /dev/null +++ b/test_e2e/http_support_test.go @@ -0,0 +1,255 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + + "github.com/cucumber/godog" + "github.com/cucumber/messages-go/v16" + "github.com/elliotchance/pie/v2" +) + +const BaseURL = "http://localhost:8000" + +func (t *TestContext) requestGet(url string) error { + return t.request(url, http.MethodGet, nil, nil) +} + +func (t *TestContext) requestWithJSON(url, method string, jsonTable *godog.Table) error { + return t.request(url, method, nil, jsonTable) +} + +func (t *TestContext) request(url, method string, query map[string]string, jsonTable *godog.Table) error { + var payload io.Reader + + var err error + + if jsonTable != nil { + payload = buildJSONFromTable(jsonTable) + } + + method, err = checkMethod(method) + if err != nil { + return err + } + + request, err := http.NewRequest(method, BaseURL+url, payload) + if err != nil { + return fmt.Errorf("failed to build request ERROR=%s", err.Error()) + } + + q := request.URL.Query() + for k, v := range query { + q.Add(k, v) + } + + request.URL.RawQuery = q.Encode() + + response, err := t.httpClient.Do(request) + if err != nil { + return fmt.Errorf("failed to run request ERROR=%s", err.Error()) + } + + t.storeResponseInContext(response) + response.Body.Close() + + return nil +} + +func (t *TestContext) storeResponseInContext(response *http.Response) { + t.statusCode = response.StatusCode + + err := json.NewDecoder(response.Body).Decode(&t.json) + if err != nil { + t.json = map[string]any{} + } +} + +func (t *TestContext) assertStatusCode(expectedStatusCode int) error { + if t.statusCode != expectedStatusCode { + return fmt.Errorf("expect status code %d but is %d", expectedStatusCode, t.statusCode) + } + + return nil +} + +func (t *TestContext) assertResponseFieldIsEquals(field string, expectedValue string) error { + fields := strings.Split(field, ".") + jsonMap := t.json.(map[string]any) + + for _, field := range fields[:len(fields)-1] { + intValue, present := jsonMap[field] + if !present { + return fmt.Errorf("expected response field %s to be %s but it is not present", field, expectedValue) + } + + jsonMap = intValue.(map[string]any) + } + + lastValue, present := jsonMap[pie.Last(fields)] + if !present { + return fmt.Errorf("expected response field %s to be %s but it is not present", field, expectedValue) + } + + if !assertValue(lastValue, expectedValue) { + return fmt.Errorf("expected response field %s to be %s but is %v", field, expectedValue, lastValue) + } + + return nil +} + +func assertValue(value any, expectedValue string) bool { + switch value.(type) { + case string: + return expectedValue == value + case int: + expectedValueInt, err := strconv.Atoi(expectedValue) + if err != nil { + panic(err) + } + + return expectedValueInt == value + case float64: + expectedValueFloat, err := strconv.ParseFloat(expectedValue, 64) + if err != nil { + panic(err) + } + + return expectedValueFloat == value + default: + panic("unsupported format") + } +} + +// build a map from a godog.Table +func buildMapFromTable(table *godog.Table) (map[string]any, error) { + data := make(map[string]any, 0) + + err := verifyHeader(table.Rows[0]) + if err != nil { + return nil, err + } + + for _, row := range table.Rows[1:] { + key := row.Cells[0].Value + valueAsString := row.Cells[1].Value + valueType := row.Cells[2].Value + + value, err := getTableValue(key, valueAsString, valueType) + if err != nil { + return nil, err + } + + data[key] = value + } + + return data, nil +} + +// Verifies that the header row of a table has the correct format +func verifyHeader(row *messages.PickleTableRow) error { + for indexCell, cell := range row.Cells { + if cell.Value != []string{"key", "value", "type"}[indexCell] { + return fmt.Errorf("should have %q as first line of the table", "| key | value | type |") + } + } + + return nil +} + +// Returns the value present in a table casted to the correct type +func getTableValue(key, valueAsString, valueType string) (any, error) { + switch valueType { + case stringValueType: + return valueAsString, nil + case booleanValueType: + boolean, err := strconv.ParseBool(valueAsString) + if err != nil { + return nil, fmt.Errorf("can't parse %q as boolean for key %q", valueAsString, key) + } + + return boolean, nil + case integerValueType: + integer, err := strconv.ParseInt(valueAsString, 10, 64) + if err != nil { + return nil, fmt.Errorf("can't parse %q as integer for key %q", valueAsString, key) + } + + return integer, nil + case floatValueType: + floatingNumber, err := strconv.ParseFloat(valueAsString, 64) + if err != nil { + return nil, fmt.Errorf("can't parse %q as float for key %q", valueAsString, key) + } + + return floatingNumber, nil + case jsonValueType: + jsonMap := map[string]string{} + + err := json.Unmarshal([]byte(valueAsString), &jsonMap) + if err != nil { + return nil, fmt.Errorf("can't parse %q as json for key %q", valueAsString, key) + } + + return jsonMap, nil + default: + return nil, fmt.Errorf( + "type %q does not exists, please use %v", + valueType, + []string{stringValueType, booleanValueType, integerValueType, floatValueType}, + ) + } +} + +// build a json payload in the form of a reader from a godog.Table +func buildJSONFromTable(table *godog.Table) io.Reader { + data, err := buildMapFromTable(table) + if err != nil { + panic("should not return an error") + } + + bytes, err := json.Marshal(data) + if err != nil { + panic("should not return an error") + } + + return strings.NewReader(string(bytes)) +} + +const ( + stringValueType = "string" + booleanValueType = "boolean" + integerValueType = "integer" + floatValueType = "float" + nullValueType = "null" + jsonValueType = "json" +) + +// check if the method is allowed and sanitize the string +func checkMethod(method string) (string, error) { + allowedMethods := []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodOptions, + http.MethodTrace, + } + sanitizedMethod := strings.TrimSpace(strings.ToUpper(method)) + + if !pie.Contains( + allowedMethods, + sanitizedMethod, + ) { + return "", fmt.Errorf("%q is not a valid HTTP method (please choose between %v)", method, allowedMethods) + } + + return sanitizedMethod, nil +} diff --git a/test_e2e/setup.go b/test_e2e/setup.go new file mode 100644 index 00000000..8339f29a --- /dev/null +++ b/test_e2e/setup.go @@ -0,0 +1,17 @@ +package main + +import ( + "gorm.io/gorm" + + "github.com/ditrit/badaas/persistence/models" + "github.com/ditrit/badaas/testintegration" +) + +func CleanDB(db *gorm.DB) { + testintegration.CleanDBTables(db, + []any{ + models.Session{}, + models.User{}, + }, + ) +} diff --git a/test_e2e/test_api.go b/test_e2e/test_api.go new file mode 100644 index 00000000..c8ab617d --- /dev/null +++ b/test_e2e/test_api.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/Masterminds/semver/v3" + + "github.com/ditrit/badaas" +) + +func main() { + badaas.BaDaaS.AddModules( + badaas.InfoModule, + badaas.AuthModule, + ).Provide( + NewAPIVersion, + ).Start() +} + +func NewAPIVersion() *semver.Version { + return semver.MustParse("0.0.0-unreleased") +} diff --git a/testintegration/asserts.go b/testintegration/asserts.go new file mode 100644 index 00000000..6e514505 --- /dev/null +++ b/testintegration/asserts.go @@ -0,0 +1,35 @@ +package testintegration + +import ( + "log" + + "github.com/stretchr/testify/suite" + is "gotest.tools/assert/cmp" +) + +func EqualList[T any](ts *suite.Suite, expectedList, actualList []T) { + expectedLen := len(expectedList) + equalLen := ts.Len(actualList, expectedLen) + + if equalLen { + for i := 0; i < expectedLen; i++ { + j := 0 + for ; j < expectedLen; j++ { + if is.DeepEqual( + actualList[j], + expectedList[i], + )().Success() { + break + } + } + + if j == expectedLen { + for _, element := range actualList { + log.Println(element) + } + + ts.FailNow("Lists not equal", "element %v not in list %v", expectedList[i], actualList) + } + } + } +} diff --git a/testintegration/auth_service.go b/testintegration/auth_service.go new file mode 100644 index 00000000..47faccbb --- /dev/null +++ b/testintegration/auth_service.go @@ -0,0 +1,50 @@ +package testintegration + +import ( + "github.com/stretchr/testify/suite" + "gorm.io/gorm" + + "github.com/ditrit/badaas/persistence/models/dto" + "github.com/ditrit/badaas/services/userservice" +) + +type AuthServiceIntTestSuite struct { + suite.Suite + db *gorm.DB + userService userservice.UserService +} + +func NewAuthServiceIntTestSuite( + db *gorm.DB, + userService userservice.UserService, +) *AuthServiceIntTestSuite { + return &AuthServiceIntTestSuite{ + db: db, + userService: userService, + } +} + +func (ts *AuthServiceIntTestSuite) SetupTest() { + CleanDB(ts.db) +} + +func (ts *AuthServiceIntTestSuite) TearDownSuite() { + CleanDB(ts.db) +} + +func (ts *AuthServiceIntTestSuite) TestGetUser() { + email := "franco@ditrit.io" + password := "1234" + + _, err := ts.userService.NewUser("franco", email, password) + ts.Nil(err) + + user, err := ts.userService.GetUser(dto.UserLoginDTO{ + Email: email, + Password: password, + }) + ts.Nil(err) + ts.Equal(user.Username, "franco") + ts.Equal(user.Email, email) + ts.NotEqual(user.Password, password) +} diff --git a/testintegration/badaas_test.go b/testintegration/badaas_test.go new file mode 100644 index 00000000..bf1550f8 --- /dev/null +++ b/testintegration/badaas_test.go @@ -0,0 +1,54 @@ +//go:build cockroachdb +// +build cockroachdb + +package testintegration + +import ( + "path" + "path/filepath" + "runtime" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/suite" + "go.uber.org/fx" + + "github.com/ditrit/badaas" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/persistence/repository" + "github.com/ditrit/badaas/testintegration/models" +) + +func TestBaDaaS(t *testing.T) { + _, b, _, _ := runtime.Caller(0) + basePath := filepath.Dir(b) + + viper.Set("config_path", path.Join(basePath, "int_test_config.yml")) + + tGlobal = t + + badaas.BaDaaS.AddModules( + badaas.AuthModule, + ).Provide( + // provide test suites + GetModels, + repository.NewCRUD[models.Product, model.UUID], + // create test suites + NewCRUDRepositoryIntTestSuite, + NewAuthServiceIntTestSuite, + ).Invoke( + // run tests + runTestSuites, + ).Start() +} + +func runTestSuites( + tsCRUDRepository *CRUDRepositoryIntTestSuite, + tsAuthService *AuthServiceIntTestSuite, + shutdowner fx.Shutdowner, +) { + suite.Run(tGlobal, tsCRUDRepository) + suite.Run(tGlobal, tsAuthService) + + shutdowner.Shutdown() +} diff --git a/testintegration/conditions/bicycle_conditions.go b/testintegration/conditions/bicycle_conditions.go new file mode 100644 index 00000000..29fec61b --- /dev/null +++ b/testintegration/conditions/bicycle_conditions.go @@ -0,0 +1,85 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var bicycleType = reflect.TypeOf(*new(models.Bicycle)) + +func (bicycleConditions bicycleConditions) IdIs() orm.FieldIs[models.Bicycle, model.UUID] { + return orm.FieldIs[models.Bicycle, model.UUID]{FieldID: bicycleConditions.ID} +} +func (bicycleConditions bicycleConditions) CreatedAtIs() orm.FieldIs[models.Bicycle, time.Time] { + return orm.FieldIs[models.Bicycle, time.Time]{FieldID: bicycleConditions.CreatedAt} +} +func (bicycleConditions bicycleConditions) UpdatedAtIs() orm.FieldIs[models.Bicycle, time.Time] { + return orm.FieldIs[models.Bicycle, time.Time]{FieldID: bicycleConditions.UpdatedAt} +} +func (bicycleConditions bicycleConditions) DeletedAtIs() orm.FieldIs[models.Bicycle, time.Time] { + return orm.FieldIs[models.Bicycle, time.Time]{FieldID: bicycleConditions.DeletedAt} +} +func (bicycleConditions bicycleConditions) NameIs() orm.StringFieldIs[models.Bicycle] { + return orm.StringFieldIs[models.Bicycle]{FieldIs: orm.FieldIs[models.Bicycle, string]{FieldID: bicycleConditions.Name}} +} +func (bicycleConditions bicycleConditions) Owner(conditions ...condition.Condition[models.Person]) condition.JoinCondition[models.Bicycle] { + return condition.NewJoinCondition[models.Bicycle, models.Person](conditions, "Owner", "OwnerName", bicycleConditions.Preload(), "Name") +} +func (bicycleConditions bicycleConditions) PreloadOwner() condition.JoinCondition[models.Bicycle] { + return bicycleConditions.Owner(Person.Preload()) +} +func (bicycleConditions bicycleConditions) OwnerNameIs() orm.StringFieldIs[models.Bicycle] { + return orm.StringFieldIs[models.Bicycle]{FieldIs: orm.FieldIs[models.Bicycle, string]{FieldID: bicycleConditions.OwnerName}} +} + +type bicycleConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] + OwnerName query.FieldIdentifier[string] +} + +var Bicycle = bicycleConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: bicycleType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: bicycleType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: bicycleType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: bicycleType, + }, + OwnerName: query.FieldIdentifier[string]{ + Field: "OwnerName", + ModelType: bicycleType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: bicycleType, + }, +} + +// Preload allows preloading the Bicycle when doing a query +func (bicycleConditions bicycleConditions) Preload() condition.Condition[models.Bicycle] { + return condition.NewPreloadCondition[models.Bicycle](bicycleConditions.ID, bicycleConditions.CreatedAt, bicycleConditions.UpdatedAt, bicycleConditions.DeletedAt, bicycleConditions.Name, bicycleConditions.OwnerName) +} + +// PreloadRelations allows preloading all the Bicycle's relation when doing a query +func (bicycleConditions bicycleConditions) PreloadRelations() []condition.Condition[models.Bicycle] { + return []condition.Condition[models.Bicycle]{bicycleConditions.PreloadOwner()} +} diff --git a/testintegration/conditions/brand_conditions.go b/testintegration/conditions/brand_conditions.go new file mode 100644 index 00000000..7ea34fec --- /dev/null +++ b/testintegration/conditions/brand_conditions.go @@ -0,0 +1,66 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var brandType = reflect.TypeOf(*new(models.Brand)) + +func (brandConditions brandConditions) IdIs() orm.FieldIs[models.Brand, model.UIntID] { + return orm.FieldIs[models.Brand, model.UIntID]{FieldID: brandConditions.ID} +} +func (brandConditions brandConditions) CreatedAtIs() orm.FieldIs[models.Brand, time.Time] { + return orm.FieldIs[models.Brand, time.Time]{FieldID: brandConditions.CreatedAt} +} +func (brandConditions brandConditions) UpdatedAtIs() orm.FieldIs[models.Brand, time.Time] { + return orm.FieldIs[models.Brand, time.Time]{FieldID: brandConditions.UpdatedAt} +} +func (brandConditions brandConditions) DeletedAtIs() orm.FieldIs[models.Brand, time.Time] { + return orm.FieldIs[models.Brand, time.Time]{FieldID: brandConditions.DeletedAt} +} +func (brandConditions brandConditions) NameIs() orm.StringFieldIs[models.Brand] { + return orm.StringFieldIs[models.Brand]{FieldIs: orm.FieldIs[models.Brand, string]{FieldID: brandConditions.Name}} +} + +type brandConditions struct { + ID query.FieldIdentifier[model.UIntID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] +} + +var Brand = brandConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: brandType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: brandType, + }, + ID: query.FieldIdentifier[model.UIntID]{ + Field: "ID", + ModelType: brandType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: brandType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: brandType, + }, +} + +// Preload allows preloading the Brand when doing a query +func (brandConditions brandConditions) Preload() condition.Condition[models.Brand] { + return condition.NewPreloadCondition[models.Brand](brandConditions.ID, brandConditions.CreatedAt, brandConditions.UpdatedAt, brandConditions.DeletedAt, brandConditions.Name) +} diff --git a/testintegration/conditions/child_conditions.go b/testintegration/conditions/child_conditions.go new file mode 100644 index 00000000..de2b9f7f --- /dev/null +++ b/testintegration/conditions/child_conditions.go @@ -0,0 +1,107 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var childType = reflect.TypeOf(*new(models.Child)) + +func (childConditions childConditions) IdIs() orm.FieldIs[models.Child, model.UUID] { + return orm.FieldIs[models.Child, model.UUID]{FieldID: childConditions.ID} +} +func (childConditions childConditions) CreatedAtIs() orm.FieldIs[models.Child, time.Time] { + return orm.FieldIs[models.Child, time.Time]{FieldID: childConditions.CreatedAt} +} +func (childConditions childConditions) UpdatedAtIs() orm.FieldIs[models.Child, time.Time] { + return orm.FieldIs[models.Child, time.Time]{FieldID: childConditions.UpdatedAt} +} +func (childConditions childConditions) DeletedAtIs() orm.FieldIs[models.Child, time.Time] { + return orm.FieldIs[models.Child, time.Time]{FieldID: childConditions.DeletedAt} +} +func (childConditions childConditions) NameIs() orm.StringFieldIs[models.Child] { + return orm.StringFieldIs[models.Child]{FieldIs: orm.FieldIs[models.Child, string]{FieldID: childConditions.Name}} +} +func (childConditions childConditions) NumberIs() orm.FieldIs[models.Child, int] { + return orm.FieldIs[models.Child, int]{FieldID: childConditions.Number} +} +func (childConditions childConditions) Parent1(conditions ...condition.Condition[models.Parent1]) condition.JoinCondition[models.Child] { + return condition.NewJoinCondition[models.Child, models.Parent1](conditions, "Parent1", "Parent1ID", childConditions.Preload(), "ID") +} +func (childConditions childConditions) PreloadParent1() condition.JoinCondition[models.Child] { + return childConditions.Parent1(Parent1.Preload()) +} +func (childConditions childConditions) Parent1IdIs() orm.FieldIs[models.Child, model.UUID] { + return orm.FieldIs[models.Child, model.UUID]{FieldID: childConditions.Parent1ID} +} +func (childConditions childConditions) Parent2(conditions ...condition.Condition[models.Parent2]) condition.JoinCondition[models.Child] { + return condition.NewJoinCondition[models.Child, models.Parent2](conditions, "Parent2", "Parent2ID", childConditions.Preload(), "ID") +} +func (childConditions childConditions) PreloadParent2() condition.JoinCondition[models.Child] { + return childConditions.Parent2(Parent2.Preload()) +} +func (childConditions childConditions) Parent2IdIs() orm.FieldIs[models.Child, model.UUID] { + return orm.FieldIs[models.Child, model.UUID]{FieldID: childConditions.Parent2ID} +} + +type childConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] + Number query.FieldIdentifier[int] + Parent1ID query.FieldIdentifier[model.UUID] + Parent2ID query.FieldIdentifier[model.UUID] +} + +var Child = childConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: childType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: childType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: childType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: childType, + }, + Number: query.FieldIdentifier[int]{ + Field: "Number", + ModelType: childType, + }, + Parent1ID: query.FieldIdentifier[model.UUID]{ + Field: "Parent1ID", + ModelType: childType, + }, + Parent2ID: query.FieldIdentifier[model.UUID]{ + Field: "Parent2ID", + ModelType: childType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: childType, + }, +} + +// Preload allows preloading the Child when doing a query +func (childConditions childConditions) Preload() condition.Condition[models.Child] { + return condition.NewPreloadCondition[models.Child](childConditions.ID, childConditions.CreatedAt, childConditions.UpdatedAt, childConditions.DeletedAt, childConditions.Name, childConditions.Number, childConditions.Parent1ID, childConditions.Parent2ID) +} + +// PreloadRelations allows preloading all the Child's relation when doing a query +func (childConditions childConditions) PreloadRelations() []condition.Condition[models.Child] { + return []condition.Condition[models.Child]{childConditions.PreloadParent1(), childConditions.PreloadParent2()} +} diff --git a/testintegration/conditions/city_conditions.go b/testintegration/conditions/city_conditions.go new file mode 100644 index 00000000..f52be26f --- /dev/null +++ b/testintegration/conditions/city_conditions.go @@ -0,0 +1,85 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var cityType = reflect.TypeOf(*new(models.City)) + +func (cityConditions cityConditions) IdIs() orm.FieldIs[models.City, model.UUID] { + return orm.FieldIs[models.City, model.UUID]{FieldID: cityConditions.ID} +} +func (cityConditions cityConditions) CreatedAtIs() orm.FieldIs[models.City, time.Time] { + return orm.FieldIs[models.City, time.Time]{FieldID: cityConditions.CreatedAt} +} +func (cityConditions cityConditions) UpdatedAtIs() orm.FieldIs[models.City, time.Time] { + return orm.FieldIs[models.City, time.Time]{FieldID: cityConditions.UpdatedAt} +} +func (cityConditions cityConditions) DeletedAtIs() orm.FieldIs[models.City, time.Time] { + return orm.FieldIs[models.City, time.Time]{FieldID: cityConditions.DeletedAt} +} +func (cityConditions cityConditions) NameIs() orm.StringFieldIs[models.City] { + return orm.StringFieldIs[models.City]{FieldIs: orm.FieldIs[models.City, string]{FieldID: cityConditions.Name}} +} +func (cityConditions cityConditions) Country(conditions ...condition.Condition[models.Country]) condition.JoinCondition[models.City] { + return condition.NewJoinCondition[models.City, models.Country](conditions, "Country", "CountryID", cityConditions.Preload(), "ID") +} +func (cityConditions cityConditions) PreloadCountry() condition.JoinCondition[models.City] { + return cityConditions.Country(Country.Preload()) +} +func (cityConditions cityConditions) CountryIdIs() orm.FieldIs[models.City, model.UUID] { + return orm.FieldIs[models.City, model.UUID]{FieldID: cityConditions.CountryID} +} + +type cityConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] + CountryID query.FieldIdentifier[model.UUID] +} + +var City = cityConditions{ + CountryID: query.FieldIdentifier[model.UUID]{ + Field: "CountryID", + ModelType: cityType, + }, + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: cityType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: cityType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: cityType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: cityType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: cityType, + }, +} + +// Preload allows preloading the City when doing a query +func (cityConditions cityConditions) Preload() condition.Condition[models.City] { + return condition.NewPreloadCondition[models.City](cityConditions.ID, cityConditions.CreatedAt, cityConditions.UpdatedAt, cityConditions.DeletedAt, cityConditions.Name, cityConditions.CountryID) +} + +// PreloadRelations allows preloading all the City's relation when doing a query +func (cityConditions cityConditions) PreloadRelations() []condition.Condition[models.City] { + return []condition.Condition[models.City]{cityConditions.PreloadCountry()} +} diff --git a/testintegration/conditions/company_conditions.go b/testintegration/conditions/company_conditions.go new file mode 100644 index 00000000..49a8eb4c --- /dev/null +++ b/testintegration/conditions/company_conditions.go @@ -0,0 +1,74 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var companyType = reflect.TypeOf(*new(models.Company)) + +func (companyConditions companyConditions) IdIs() orm.FieldIs[models.Company, model.UUID] { + return orm.FieldIs[models.Company, model.UUID]{FieldID: companyConditions.ID} +} +func (companyConditions companyConditions) CreatedAtIs() orm.FieldIs[models.Company, time.Time] { + return orm.FieldIs[models.Company, time.Time]{FieldID: companyConditions.CreatedAt} +} +func (companyConditions companyConditions) UpdatedAtIs() orm.FieldIs[models.Company, time.Time] { + return orm.FieldIs[models.Company, time.Time]{FieldID: companyConditions.UpdatedAt} +} +func (companyConditions companyConditions) DeletedAtIs() orm.FieldIs[models.Company, time.Time] { + return orm.FieldIs[models.Company, time.Time]{FieldID: companyConditions.DeletedAt} +} +func (companyConditions companyConditions) NameIs() orm.StringFieldIs[models.Company] { + return orm.StringFieldIs[models.Company]{FieldIs: orm.FieldIs[models.Company, string]{FieldID: companyConditions.Name}} +} +func (companyConditions companyConditions) PreloadSellers(nestedPreloads ...condition.JoinCondition[models.Seller]) condition.Condition[models.Company] { + return condition.NewCollectionPreloadCondition[models.Company, models.Seller]("Sellers", nestedPreloads) +} + +type companyConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] +} + +var Company = companyConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: companyType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: companyType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: companyType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: companyType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: companyType, + }, +} + +// Preload allows preloading the Company when doing a query +func (companyConditions companyConditions) Preload() condition.Condition[models.Company] { + return condition.NewPreloadCondition[models.Company](companyConditions.ID, companyConditions.CreatedAt, companyConditions.UpdatedAt, companyConditions.DeletedAt, companyConditions.Name) +} + +// PreloadRelations allows preloading all the Company's relation when doing a query +func (companyConditions companyConditions) PreloadRelations() []condition.Condition[models.Company] { + return []condition.Condition[models.Company]{companyConditions.PreloadSellers()} +} diff --git a/testintegration/conditions/country_conditions.go b/testintegration/conditions/country_conditions.go new file mode 100644 index 00000000..1e793280 --- /dev/null +++ b/testintegration/conditions/country_conditions.go @@ -0,0 +1,77 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var countryType = reflect.TypeOf(*new(models.Country)) + +func (countryConditions countryConditions) IdIs() orm.FieldIs[models.Country, model.UUID] { + return orm.FieldIs[models.Country, model.UUID]{FieldID: countryConditions.ID} +} +func (countryConditions countryConditions) CreatedAtIs() orm.FieldIs[models.Country, time.Time] { + return orm.FieldIs[models.Country, time.Time]{FieldID: countryConditions.CreatedAt} +} +func (countryConditions countryConditions) UpdatedAtIs() orm.FieldIs[models.Country, time.Time] { + return orm.FieldIs[models.Country, time.Time]{FieldID: countryConditions.UpdatedAt} +} +func (countryConditions countryConditions) DeletedAtIs() orm.FieldIs[models.Country, time.Time] { + return orm.FieldIs[models.Country, time.Time]{FieldID: countryConditions.DeletedAt} +} +func (countryConditions countryConditions) NameIs() orm.StringFieldIs[models.Country] { + return orm.StringFieldIs[models.Country]{FieldIs: orm.FieldIs[models.Country, string]{FieldID: countryConditions.Name}} +} +func (countryConditions countryConditions) Capital(conditions ...condition.Condition[models.City]) condition.JoinCondition[models.Country] { + return condition.NewJoinCondition[models.Country, models.City](conditions, "Capital", "ID", countryConditions.Preload(), "CountryID") +} +func (countryConditions countryConditions) PreloadCapital() condition.JoinCondition[models.Country] { + return countryConditions.Capital(City.Preload()) +} + +type countryConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] +} + +var Country = countryConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: countryType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: countryType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: countryType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: countryType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: countryType, + }, +} + +// Preload allows preloading the Country when doing a query +func (countryConditions countryConditions) Preload() condition.Condition[models.Country] { + return condition.NewPreloadCondition[models.Country](countryConditions.ID, countryConditions.CreatedAt, countryConditions.UpdatedAt, countryConditions.DeletedAt, countryConditions.Name) +} + +// PreloadRelations allows preloading all the Country's relation when doing a query +func (countryConditions countryConditions) PreloadRelations() []condition.Condition[models.Country] { + return []condition.Condition[models.Country]{countryConditions.PreloadCapital()} +} diff --git a/testintegration/conditions/employee_conditions.go b/testintegration/conditions/employee_conditions.go new file mode 100644 index 00000000..339843e3 --- /dev/null +++ b/testintegration/conditions/employee_conditions.go @@ -0,0 +1,85 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var employeeType = reflect.TypeOf(*new(models.Employee)) + +func (employeeConditions employeeConditions) IdIs() orm.FieldIs[models.Employee, model.UUID] { + return orm.FieldIs[models.Employee, model.UUID]{FieldID: employeeConditions.ID} +} +func (employeeConditions employeeConditions) CreatedAtIs() orm.FieldIs[models.Employee, time.Time] { + return orm.FieldIs[models.Employee, time.Time]{FieldID: employeeConditions.CreatedAt} +} +func (employeeConditions employeeConditions) UpdatedAtIs() orm.FieldIs[models.Employee, time.Time] { + return orm.FieldIs[models.Employee, time.Time]{FieldID: employeeConditions.UpdatedAt} +} +func (employeeConditions employeeConditions) DeletedAtIs() orm.FieldIs[models.Employee, time.Time] { + return orm.FieldIs[models.Employee, time.Time]{FieldID: employeeConditions.DeletedAt} +} +func (employeeConditions employeeConditions) NameIs() orm.StringFieldIs[models.Employee] { + return orm.StringFieldIs[models.Employee]{FieldIs: orm.FieldIs[models.Employee, string]{FieldID: employeeConditions.Name}} +} +func (employeeConditions employeeConditions) Boss(conditions ...condition.Condition[models.Employee]) condition.JoinCondition[models.Employee] { + return condition.NewJoinCondition[models.Employee, models.Employee](conditions, "Boss", "BossID", employeeConditions.Preload(), "ID") +} +func (employeeConditions employeeConditions) PreloadBoss() condition.JoinCondition[models.Employee] { + return employeeConditions.Boss(Employee.Preload()) +} +func (employeeConditions employeeConditions) BossIdIs() orm.FieldIs[models.Employee, model.UUID] { + return orm.FieldIs[models.Employee, model.UUID]{FieldID: employeeConditions.BossID} +} + +type employeeConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] + BossID query.FieldIdentifier[model.UUID] +} + +var Employee = employeeConditions{ + BossID: query.FieldIdentifier[model.UUID]{ + Field: "BossID", + ModelType: employeeType, + }, + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: employeeType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: employeeType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: employeeType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: employeeType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: employeeType, + }, +} + +// Preload allows preloading the Employee when doing a query +func (employeeConditions employeeConditions) Preload() condition.Condition[models.Employee] { + return condition.NewPreloadCondition[models.Employee](employeeConditions.ID, employeeConditions.CreatedAt, employeeConditions.UpdatedAt, employeeConditions.DeletedAt, employeeConditions.Name, employeeConditions.BossID) +} + +// PreloadRelations allows preloading all the Employee's relation when doing a query +func (employeeConditions employeeConditions) PreloadRelations() []condition.Condition[models.Employee] { + return []condition.Condition[models.Employee]{employeeConditions.PreloadBoss()} +} diff --git a/testintegration/conditions/orm.go b/testintegration/conditions/orm.go new file mode 100644 index 00000000..78acbc6d --- /dev/null +++ b/testintegration/conditions/orm.go @@ -0,0 +1,3 @@ +package conditions + +//go:generate badaas-cli gen conditions ../models diff --git a/testintegration/conditions/parent1_conditions.go b/testintegration/conditions/parent1_conditions.go new file mode 100644 index 00000000..5822db2d --- /dev/null +++ b/testintegration/conditions/parent1_conditions.go @@ -0,0 +1,77 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var parent1Type = reflect.TypeOf(*new(models.Parent1)) + +func (parent1Conditions parent1Conditions) IdIs() orm.FieldIs[models.Parent1, model.UUID] { + return orm.FieldIs[models.Parent1, model.UUID]{FieldID: parent1Conditions.ID} +} +func (parent1Conditions parent1Conditions) CreatedAtIs() orm.FieldIs[models.Parent1, time.Time] { + return orm.FieldIs[models.Parent1, time.Time]{FieldID: parent1Conditions.CreatedAt} +} +func (parent1Conditions parent1Conditions) UpdatedAtIs() orm.FieldIs[models.Parent1, time.Time] { + return orm.FieldIs[models.Parent1, time.Time]{FieldID: parent1Conditions.UpdatedAt} +} +func (parent1Conditions parent1Conditions) DeletedAtIs() orm.FieldIs[models.Parent1, time.Time] { + return orm.FieldIs[models.Parent1, time.Time]{FieldID: parent1Conditions.DeletedAt} +} +func (parent1Conditions parent1Conditions) ParentParent(conditions ...condition.Condition[models.ParentParent]) condition.JoinCondition[models.Parent1] { + return condition.NewJoinCondition[models.Parent1, models.ParentParent](conditions, "ParentParent", "ParentParentID", parent1Conditions.Preload(), "ID") +} +func (parent1Conditions parent1Conditions) PreloadParentParent() condition.JoinCondition[models.Parent1] { + return parent1Conditions.ParentParent(ParentParent.Preload()) +} +func (parent1Conditions parent1Conditions) ParentParentIdIs() orm.FieldIs[models.Parent1, model.UUID] { + return orm.FieldIs[models.Parent1, model.UUID]{FieldID: parent1Conditions.ParentParentID} +} + +type parent1Conditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + ParentParentID query.FieldIdentifier[model.UUID] +} + +var Parent1 = parent1Conditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: parent1Type, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: parent1Type, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: parent1Type, + }, + ParentParentID: query.FieldIdentifier[model.UUID]{ + Field: "ParentParentID", + ModelType: parent1Type, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: parent1Type, + }, +} + +// Preload allows preloading the Parent1 when doing a query +func (parent1Conditions parent1Conditions) Preload() condition.Condition[models.Parent1] { + return condition.NewPreloadCondition[models.Parent1](parent1Conditions.ID, parent1Conditions.CreatedAt, parent1Conditions.UpdatedAt, parent1Conditions.DeletedAt, parent1Conditions.ParentParentID) +} + +// PreloadRelations allows preloading all the Parent1's relation when doing a query +func (parent1Conditions parent1Conditions) PreloadRelations() []condition.Condition[models.Parent1] { + return []condition.Condition[models.Parent1]{parent1Conditions.PreloadParentParent()} +} diff --git a/testintegration/conditions/parent2_conditions.go b/testintegration/conditions/parent2_conditions.go new file mode 100644 index 00000000..f2da44b5 --- /dev/null +++ b/testintegration/conditions/parent2_conditions.go @@ -0,0 +1,77 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var parent2Type = reflect.TypeOf(*new(models.Parent2)) + +func (parent2Conditions parent2Conditions) IdIs() orm.FieldIs[models.Parent2, model.UUID] { + return orm.FieldIs[models.Parent2, model.UUID]{FieldID: parent2Conditions.ID} +} +func (parent2Conditions parent2Conditions) CreatedAtIs() orm.FieldIs[models.Parent2, time.Time] { + return orm.FieldIs[models.Parent2, time.Time]{FieldID: parent2Conditions.CreatedAt} +} +func (parent2Conditions parent2Conditions) UpdatedAtIs() orm.FieldIs[models.Parent2, time.Time] { + return orm.FieldIs[models.Parent2, time.Time]{FieldID: parent2Conditions.UpdatedAt} +} +func (parent2Conditions parent2Conditions) DeletedAtIs() orm.FieldIs[models.Parent2, time.Time] { + return orm.FieldIs[models.Parent2, time.Time]{FieldID: parent2Conditions.DeletedAt} +} +func (parent2Conditions parent2Conditions) ParentParent(conditions ...condition.Condition[models.ParentParent]) condition.JoinCondition[models.Parent2] { + return condition.NewJoinCondition[models.Parent2, models.ParentParent](conditions, "ParentParent", "ParentParentID", parent2Conditions.Preload(), "ID") +} +func (parent2Conditions parent2Conditions) PreloadParentParent() condition.JoinCondition[models.Parent2] { + return parent2Conditions.ParentParent(ParentParent.Preload()) +} +func (parent2Conditions parent2Conditions) ParentParentIdIs() orm.FieldIs[models.Parent2, model.UUID] { + return orm.FieldIs[models.Parent2, model.UUID]{FieldID: parent2Conditions.ParentParentID} +} + +type parent2Conditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + ParentParentID query.FieldIdentifier[model.UUID] +} + +var Parent2 = parent2Conditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: parent2Type, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: parent2Type, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: parent2Type, + }, + ParentParentID: query.FieldIdentifier[model.UUID]{ + Field: "ParentParentID", + ModelType: parent2Type, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: parent2Type, + }, +} + +// Preload allows preloading the Parent2 when doing a query +func (parent2Conditions parent2Conditions) Preload() condition.Condition[models.Parent2] { + return condition.NewPreloadCondition[models.Parent2](parent2Conditions.ID, parent2Conditions.CreatedAt, parent2Conditions.UpdatedAt, parent2Conditions.DeletedAt, parent2Conditions.ParentParentID) +} + +// PreloadRelations allows preloading all the Parent2's relation when doing a query +func (parent2Conditions parent2Conditions) PreloadRelations() []condition.Condition[models.Parent2] { + return []condition.Condition[models.Parent2]{parent2Conditions.PreloadParentParent()} +} diff --git a/testintegration/conditions/parent_parent_conditions.go b/testintegration/conditions/parent_parent_conditions.go new file mode 100644 index 00000000..923c2d3c --- /dev/null +++ b/testintegration/conditions/parent_parent_conditions.go @@ -0,0 +1,74 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var parentParentType = reflect.TypeOf(*new(models.ParentParent)) + +func (parentParentConditions parentParentConditions) IdIs() orm.FieldIs[models.ParentParent, model.UUID] { + return orm.FieldIs[models.ParentParent, model.UUID]{FieldID: parentParentConditions.ID} +} +func (parentParentConditions parentParentConditions) CreatedAtIs() orm.FieldIs[models.ParentParent, time.Time] { + return orm.FieldIs[models.ParentParent, time.Time]{FieldID: parentParentConditions.CreatedAt} +} +func (parentParentConditions parentParentConditions) UpdatedAtIs() orm.FieldIs[models.ParentParent, time.Time] { + return orm.FieldIs[models.ParentParent, time.Time]{FieldID: parentParentConditions.UpdatedAt} +} +func (parentParentConditions parentParentConditions) DeletedAtIs() orm.FieldIs[models.ParentParent, time.Time] { + return orm.FieldIs[models.ParentParent, time.Time]{FieldID: parentParentConditions.DeletedAt} +} +func (parentParentConditions parentParentConditions) NameIs() orm.StringFieldIs[models.ParentParent] { + return orm.StringFieldIs[models.ParentParent]{FieldIs: orm.FieldIs[models.ParentParent, string]{FieldID: parentParentConditions.Name}} +} +func (parentParentConditions parentParentConditions) NumberIs() orm.FieldIs[models.ParentParent, int] { + return orm.FieldIs[models.ParentParent, int]{FieldID: parentParentConditions.Number} +} + +type parentParentConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] + Number query.FieldIdentifier[int] +} + +var ParentParent = parentParentConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: parentParentType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: parentParentType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: parentParentType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: parentParentType, + }, + Number: query.FieldIdentifier[int]{ + Field: "Number", + ModelType: parentParentType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: parentParentType, + }, +} + +// Preload allows preloading the ParentParent when doing a query +func (parentParentConditions parentParentConditions) Preload() condition.Condition[models.ParentParent] { + return condition.NewPreloadCondition[models.ParentParent](parentParentConditions.ID, parentParentConditions.CreatedAt, parentParentConditions.UpdatedAt, parentParentConditions.DeletedAt, parentParentConditions.Name, parentParentConditions.Number) +} diff --git a/testintegration/conditions/person_conditions.go b/testintegration/conditions/person_conditions.go new file mode 100644 index 00000000..5128f0b5 --- /dev/null +++ b/testintegration/conditions/person_conditions.go @@ -0,0 +1,66 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var personType = reflect.TypeOf(*new(models.Person)) + +func (personConditions personConditions) IdIs() orm.FieldIs[models.Person, model.UUID] { + return orm.FieldIs[models.Person, model.UUID]{FieldID: personConditions.ID} +} +func (personConditions personConditions) CreatedAtIs() orm.FieldIs[models.Person, time.Time] { + return orm.FieldIs[models.Person, time.Time]{FieldID: personConditions.CreatedAt} +} +func (personConditions personConditions) UpdatedAtIs() orm.FieldIs[models.Person, time.Time] { + return orm.FieldIs[models.Person, time.Time]{FieldID: personConditions.UpdatedAt} +} +func (personConditions personConditions) DeletedAtIs() orm.FieldIs[models.Person, time.Time] { + return orm.FieldIs[models.Person, time.Time]{FieldID: personConditions.DeletedAt} +} +func (personConditions personConditions) NameIs() orm.StringFieldIs[models.Person] { + return orm.StringFieldIs[models.Person]{FieldIs: orm.FieldIs[models.Person, string]{FieldID: personConditions.Name}} +} + +type personConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] +} + +var Person = personConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: personType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: personType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: personType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: personType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: personType, + }, +} + +// Preload allows preloading the Person when doing a query +func (personConditions personConditions) Preload() condition.Condition[models.Person] { + return condition.NewPreloadCondition[models.Person](personConditions.ID, personConditions.CreatedAt, personConditions.UpdatedAt, personConditions.DeletedAt, personConditions.Name) +} diff --git a/testintegration/conditions/phone_conditions.go b/testintegration/conditions/phone_conditions.go new file mode 100644 index 00000000..f602ec07 --- /dev/null +++ b/testintegration/conditions/phone_conditions.go @@ -0,0 +1,85 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var phoneType = reflect.TypeOf(*new(models.Phone)) + +func (phoneConditions phoneConditions) IdIs() orm.FieldIs[models.Phone, model.UIntID] { + return orm.FieldIs[models.Phone, model.UIntID]{FieldID: phoneConditions.ID} +} +func (phoneConditions phoneConditions) CreatedAtIs() orm.FieldIs[models.Phone, time.Time] { + return orm.FieldIs[models.Phone, time.Time]{FieldID: phoneConditions.CreatedAt} +} +func (phoneConditions phoneConditions) UpdatedAtIs() orm.FieldIs[models.Phone, time.Time] { + return orm.FieldIs[models.Phone, time.Time]{FieldID: phoneConditions.UpdatedAt} +} +func (phoneConditions phoneConditions) DeletedAtIs() orm.FieldIs[models.Phone, time.Time] { + return orm.FieldIs[models.Phone, time.Time]{FieldID: phoneConditions.DeletedAt} +} +func (phoneConditions phoneConditions) NameIs() orm.StringFieldIs[models.Phone] { + return orm.StringFieldIs[models.Phone]{FieldIs: orm.FieldIs[models.Phone, string]{FieldID: phoneConditions.Name}} +} +func (phoneConditions phoneConditions) Brand(conditions ...condition.Condition[models.Brand]) condition.JoinCondition[models.Phone] { + return condition.NewJoinCondition[models.Phone, models.Brand](conditions, "Brand", "BrandID", phoneConditions.Preload(), "ID") +} +func (phoneConditions phoneConditions) PreloadBrand() condition.JoinCondition[models.Phone] { + return phoneConditions.Brand(Brand.Preload()) +} +func (phoneConditions phoneConditions) BrandIdIs() orm.FieldIs[models.Phone, uint] { + return orm.FieldIs[models.Phone, uint]{FieldID: phoneConditions.BrandID} +} + +type phoneConditions struct { + ID query.FieldIdentifier[model.UIntID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] + BrandID query.FieldIdentifier[uint] +} + +var Phone = phoneConditions{ + BrandID: query.FieldIdentifier[uint]{ + Field: "BrandID", + ModelType: phoneType, + }, + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: phoneType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: phoneType, + }, + ID: query.FieldIdentifier[model.UIntID]{ + Field: "ID", + ModelType: phoneType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: phoneType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: phoneType, + }, +} + +// Preload allows preloading the Phone when doing a query +func (phoneConditions phoneConditions) Preload() condition.Condition[models.Phone] { + return condition.NewPreloadCondition[models.Phone](phoneConditions.ID, phoneConditions.CreatedAt, phoneConditions.UpdatedAt, phoneConditions.DeletedAt, phoneConditions.Name, phoneConditions.BrandID) +} + +// PreloadRelations allows preloading all the Phone's relation when doing a query +func (phoneConditions phoneConditions) PreloadRelations() []condition.Condition[models.Phone] { + return []condition.Condition[models.Phone]{phoneConditions.PreloadBrand()} +} diff --git a/testintegration/conditions/product_conditions.go b/testintegration/conditions/product_conditions.go new file mode 100644 index 00000000..591b4521 --- /dev/null +++ b/testintegration/conditions/product_conditions.go @@ -0,0 +1,148 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var productType = reflect.TypeOf(*new(models.Product)) + +func (productConditions productConditions) IdIs() orm.FieldIs[models.Product, model.UUID] { + return orm.FieldIs[models.Product, model.UUID]{FieldID: productConditions.ID} +} +func (productConditions productConditions) CreatedAtIs() orm.FieldIs[models.Product, time.Time] { + return orm.FieldIs[models.Product, time.Time]{FieldID: productConditions.CreatedAt} +} +func (productConditions productConditions) UpdatedAtIs() orm.FieldIs[models.Product, time.Time] { + return orm.FieldIs[models.Product, time.Time]{FieldID: productConditions.UpdatedAt} +} +func (productConditions productConditions) DeletedAtIs() orm.FieldIs[models.Product, time.Time] { + return orm.FieldIs[models.Product, time.Time]{FieldID: productConditions.DeletedAt} +} +func (productConditions productConditions) StringIs() orm.StringFieldIs[models.Product] { + return orm.StringFieldIs[models.Product]{FieldIs: orm.FieldIs[models.Product, string]{FieldID: productConditions.String}} +} +func (productConditions productConditions) IntIs() orm.FieldIs[models.Product, int] { + return orm.FieldIs[models.Product, int]{FieldID: productConditions.Int} +} +func (productConditions productConditions) IntPointerIs() orm.FieldIs[models.Product, int] { + return orm.FieldIs[models.Product, int]{FieldID: productConditions.IntPointer} +} +func (productConditions productConditions) FloatIs() orm.FieldIs[models.Product, float64] { + return orm.FieldIs[models.Product, float64]{FieldID: productConditions.Float} +} +func (productConditions productConditions) NullFloatIs() orm.FieldIs[models.Product, float64] { + return orm.FieldIs[models.Product, float64]{FieldID: productConditions.NullFloat} +} +func (productConditions productConditions) BoolIs() orm.BoolFieldIs[models.Product] { + return orm.BoolFieldIs[models.Product]{FieldIs: orm.FieldIs[models.Product, bool]{FieldID: productConditions.Bool}} +} +func (productConditions productConditions) NullBoolIs() orm.BoolFieldIs[models.Product] { + return orm.BoolFieldIs[models.Product]{FieldIs: orm.FieldIs[models.Product, bool]{FieldID: productConditions.NullBool}} +} +func (productConditions productConditions) ByteArrayIs() orm.FieldIs[models.Product, []uint8] { + return orm.FieldIs[models.Product, []uint8]{FieldID: productConditions.ByteArray} +} +func (productConditions productConditions) MultiStringIs() orm.FieldIs[models.Product, models.MultiString] { + return orm.FieldIs[models.Product, models.MultiString]{FieldID: productConditions.MultiString} +} +func (productConditions productConditions) ToBeEmbeddedEmbeddedIntIs() orm.FieldIs[models.Product, int] { + return orm.FieldIs[models.Product, int]{FieldID: productConditions.ToBeEmbeddedEmbeddedInt} +} +func (productConditions productConditions) GormEmbeddedIntIs() orm.FieldIs[models.Product, int] { + return orm.FieldIs[models.Product, int]{FieldID: productConditions.GormEmbeddedInt} +} + +type productConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + String query.FieldIdentifier[string] + Int query.FieldIdentifier[int] + IntPointer query.FieldIdentifier[int] + Float query.FieldIdentifier[float64] + NullFloat query.FieldIdentifier[float64] + Bool query.FieldIdentifier[bool] + NullBool query.FieldIdentifier[bool] + ByteArray query.FieldIdentifier[[]uint8] + MultiString query.FieldIdentifier[models.MultiString] + ToBeEmbeddedEmbeddedInt query.FieldIdentifier[int] + GormEmbeddedInt query.FieldIdentifier[int] +} + +var Product = productConditions{ + Bool: query.FieldIdentifier[bool]{ + Field: "Bool", + ModelType: productType, + }, + ByteArray: query.FieldIdentifier[[]uint8]{ + Field: "ByteArray", + ModelType: productType, + }, + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: productType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: productType, + }, + Float: query.FieldIdentifier[float64]{ + Field: "Float", + ModelType: productType, + }, + GormEmbeddedInt: query.FieldIdentifier[int]{ + ColumnPrefix: "gorm_embedded_", + Field: "Int", + ModelType: productType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: productType, + }, + Int: query.FieldIdentifier[int]{ + Field: "Int", + ModelType: productType, + }, + IntPointer: query.FieldIdentifier[int]{ + Field: "IntPointer", + ModelType: productType, + }, + MultiString: query.FieldIdentifier[models.MultiString]{ + Field: "MultiString", + ModelType: productType, + }, + NullBool: query.FieldIdentifier[bool]{ + Field: "NullBool", + ModelType: productType, + }, + NullFloat: query.FieldIdentifier[float64]{ + Field: "NullFloat", + ModelType: productType, + }, + String: query.FieldIdentifier[string]{ + Column: "string_something_else", + Field: "String", + ModelType: productType, + }, + ToBeEmbeddedEmbeddedInt: query.FieldIdentifier[int]{ + Field: "EmbeddedInt", + ModelType: productType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: productType, + }, +} + +// Preload allows preloading the Product when doing a query +func (productConditions productConditions) Preload() condition.Condition[models.Product] { + return condition.NewPreloadCondition[models.Product](productConditions.ID, productConditions.CreatedAt, productConditions.UpdatedAt, productConditions.DeletedAt, productConditions.String, productConditions.Int, productConditions.IntPointer, productConditions.Float, productConditions.NullFloat, productConditions.Bool, productConditions.NullBool, productConditions.ByteArray, productConditions.MultiString, productConditions.ToBeEmbeddedEmbeddedInt, productConditions.GormEmbeddedInt) +} diff --git a/testintegration/conditions/sale_conditions.go b/testintegration/conditions/sale_conditions.go new file mode 100644 index 00000000..d6865a8e --- /dev/null +++ b/testintegration/conditions/sale_conditions.go @@ -0,0 +1,107 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var saleType = reflect.TypeOf(*new(models.Sale)) + +func (saleConditions saleConditions) IdIs() orm.FieldIs[models.Sale, model.UUID] { + return orm.FieldIs[models.Sale, model.UUID]{FieldID: saleConditions.ID} +} +func (saleConditions saleConditions) CreatedAtIs() orm.FieldIs[models.Sale, time.Time] { + return orm.FieldIs[models.Sale, time.Time]{FieldID: saleConditions.CreatedAt} +} +func (saleConditions saleConditions) UpdatedAtIs() orm.FieldIs[models.Sale, time.Time] { + return orm.FieldIs[models.Sale, time.Time]{FieldID: saleConditions.UpdatedAt} +} +func (saleConditions saleConditions) DeletedAtIs() orm.FieldIs[models.Sale, time.Time] { + return orm.FieldIs[models.Sale, time.Time]{FieldID: saleConditions.DeletedAt} +} +func (saleConditions saleConditions) CodeIs() orm.FieldIs[models.Sale, int] { + return orm.FieldIs[models.Sale, int]{FieldID: saleConditions.Code} +} +func (saleConditions saleConditions) DescriptionIs() orm.StringFieldIs[models.Sale] { + return orm.StringFieldIs[models.Sale]{FieldIs: orm.FieldIs[models.Sale, string]{FieldID: saleConditions.Description}} +} +func (saleConditions saleConditions) Product(conditions ...condition.Condition[models.Product]) condition.JoinCondition[models.Sale] { + return condition.NewJoinCondition[models.Sale, models.Product](conditions, "Product", "ProductID", saleConditions.Preload(), "ID") +} +func (saleConditions saleConditions) PreloadProduct() condition.JoinCondition[models.Sale] { + return saleConditions.Product(Product.Preload()) +} +func (saleConditions saleConditions) ProductIdIs() orm.FieldIs[models.Sale, model.UUID] { + return orm.FieldIs[models.Sale, model.UUID]{FieldID: saleConditions.ProductID} +} +func (saleConditions saleConditions) Seller(conditions ...condition.Condition[models.Seller]) condition.JoinCondition[models.Sale] { + return condition.NewJoinCondition[models.Sale, models.Seller](conditions, "Seller", "SellerID", saleConditions.Preload(), "ID") +} +func (saleConditions saleConditions) PreloadSeller() condition.JoinCondition[models.Sale] { + return saleConditions.Seller(Seller.Preload()) +} +func (saleConditions saleConditions) SellerIdIs() orm.FieldIs[models.Sale, model.UUID] { + return orm.FieldIs[models.Sale, model.UUID]{FieldID: saleConditions.SellerID} +} + +type saleConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Code query.FieldIdentifier[int] + Description query.FieldIdentifier[string] + ProductID query.FieldIdentifier[model.UUID] + SellerID query.FieldIdentifier[model.UUID] +} + +var Sale = saleConditions{ + Code: query.FieldIdentifier[int]{ + Field: "Code", + ModelType: saleType, + }, + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: saleType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: saleType, + }, + Description: query.FieldIdentifier[string]{ + Field: "Description", + ModelType: saleType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: saleType, + }, + ProductID: query.FieldIdentifier[model.UUID]{ + Field: "ProductID", + ModelType: saleType, + }, + SellerID: query.FieldIdentifier[model.UUID]{ + Field: "SellerID", + ModelType: saleType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: saleType, + }, +} + +// Preload allows preloading the Sale when doing a query +func (saleConditions saleConditions) Preload() condition.Condition[models.Sale] { + return condition.NewPreloadCondition[models.Sale](saleConditions.ID, saleConditions.CreatedAt, saleConditions.UpdatedAt, saleConditions.DeletedAt, saleConditions.Code, saleConditions.Description, saleConditions.ProductID, saleConditions.SellerID) +} + +// PreloadRelations allows preloading all the Sale's relation when doing a query +func (saleConditions saleConditions) PreloadRelations() []condition.Condition[models.Sale] { + return []condition.Condition[models.Sale]{saleConditions.PreloadProduct(), saleConditions.PreloadSeller()} +} diff --git a/testintegration/conditions/seller_conditions.go b/testintegration/conditions/seller_conditions.go new file mode 100644 index 00000000..7d342076 --- /dev/null +++ b/testintegration/conditions/seller_conditions.go @@ -0,0 +1,99 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var sellerType = reflect.TypeOf(*new(models.Seller)) + +func (sellerConditions sellerConditions) IdIs() orm.FieldIs[models.Seller, model.UUID] { + return orm.FieldIs[models.Seller, model.UUID]{FieldID: sellerConditions.ID} +} +func (sellerConditions sellerConditions) CreatedAtIs() orm.FieldIs[models.Seller, time.Time] { + return orm.FieldIs[models.Seller, time.Time]{FieldID: sellerConditions.CreatedAt} +} +func (sellerConditions sellerConditions) UpdatedAtIs() orm.FieldIs[models.Seller, time.Time] { + return orm.FieldIs[models.Seller, time.Time]{FieldID: sellerConditions.UpdatedAt} +} +func (sellerConditions sellerConditions) DeletedAtIs() orm.FieldIs[models.Seller, time.Time] { + return orm.FieldIs[models.Seller, time.Time]{FieldID: sellerConditions.DeletedAt} +} +func (sellerConditions sellerConditions) NameIs() orm.StringFieldIs[models.Seller] { + return orm.StringFieldIs[models.Seller]{FieldIs: orm.FieldIs[models.Seller, string]{FieldID: sellerConditions.Name}} +} +func (sellerConditions sellerConditions) Company(conditions ...condition.Condition[models.Company]) condition.JoinCondition[models.Seller] { + return condition.NewJoinCondition[models.Seller, models.Company](conditions, "Company", "CompanyID", sellerConditions.Preload(), "ID") +} +func (sellerConditions sellerConditions) PreloadCompany() condition.JoinCondition[models.Seller] { + return sellerConditions.Company(Company.Preload()) +} +func (sellerConditions sellerConditions) CompanyIdIs() orm.FieldIs[models.Seller, model.UUID] { + return orm.FieldIs[models.Seller, model.UUID]{FieldID: sellerConditions.CompanyID} +} +func (sellerConditions sellerConditions) University(conditions ...condition.Condition[models.University]) condition.JoinCondition[models.Seller] { + return condition.NewJoinCondition[models.Seller, models.University](conditions, "University", "UniversityID", sellerConditions.Preload(), "ID") +} +func (sellerConditions sellerConditions) PreloadUniversity() condition.JoinCondition[models.Seller] { + return sellerConditions.University(University.Preload()) +} +func (sellerConditions sellerConditions) UniversityIdIs() orm.FieldIs[models.Seller, model.UUID] { + return orm.FieldIs[models.Seller, model.UUID]{FieldID: sellerConditions.UniversityID} +} + +type sellerConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] + CompanyID query.FieldIdentifier[model.UUID] + UniversityID query.FieldIdentifier[model.UUID] +} + +var Seller = sellerConditions{ + CompanyID: query.FieldIdentifier[model.UUID]{ + Field: "CompanyID", + ModelType: sellerType, + }, + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: sellerType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: sellerType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: sellerType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: sellerType, + }, + UniversityID: query.FieldIdentifier[model.UUID]{ + Field: "UniversityID", + ModelType: sellerType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: sellerType, + }, +} + +// Preload allows preloading the Seller when doing a query +func (sellerConditions sellerConditions) Preload() condition.Condition[models.Seller] { + return condition.NewPreloadCondition[models.Seller](sellerConditions.ID, sellerConditions.CreatedAt, sellerConditions.UpdatedAt, sellerConditions.DeletedAt, sellerConditions.Name, sellerConditions.CompanyID, sellerConditions.UniversityID) +} + +// PreloadRelations allows preloading all the Seller's relation when doing a query +func (sellerConditions sellerConditions) PreloadRelations() []condition.Condition[models.Seller] { + return []condition.Condition[models.Seller]{sellerConditions.PreloadCompany(), sellerConditions.PreloadUniversity()} +} diff --git a/testintegration/conditions/university_conditions.go b/testintegration/conditions/university_conditions.go new file mode 100644 index 00000000..bd0699ab --- /dev/null +++ b/testintegration/conditions/university_conditions.go @@ -0,0 +1,66 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package conditions + +import ( + orm "github.com/ditrit/badaas/orm" + condition "github.com/ditrit/badaas/orm/condition" + model "github.com/ditrit/badaas/orm/model" + query "github.com/ditrit/badaas/orm/query" + models "github.com/ditrit/badaas/testintegration/models" + "reflect" + "time" +) + +var universityType = reflect.TypeOf(*new(models.University)) + +func (universityConditions universityConditions) IdIs() orm.FieldIs[models.University, model.UUID] { + return orm.FieldIs[models.University, model.UUID]{FieldID: universityConditions.ID} +} +func (universityConditions universityConditions) CreatedAtIs() orm.FieldIs[models.University, time.Time] { + return orm.FieldIs[models.University, time.Time]{FieldID: universityConditions.CreatedAt} +} +func (universityConditions universityConditions) UpdatedAtIs() orm.FieldIs[models.University, time.Time] { + return orm.FieldIs[models.University, time.Time]{FieldID: universityConditions.UpdatedAt} +} +func (universityConditions universityConditions) DeletedAtIs() orm.FieldIs[models.University, time.Time] { + return orm.FieldIs[models.University, time.Time]{FieldID: universityConditions.DeletedAt} +} +func (universityConditions universityConditions) NameIs() orm.StringFieldIs[models.University] { + return orm.StringFieldIs[models.University]{FieldIs: orm.FieldIs[models.University, string]{FieldID: universityConditions.Name}} +} + +type universityConditions struct { + ID query.FieldIdentifier[model.UUID] + CreatedAt query.FieldIdentifier[time.Time] + UpdatedAt query.FieldIdentifier[time.Time] + DeletedAt query.FieldIdentifier[time.Time] + Name query.FieldIdentifier[string] +} + +var University = universityConditions{ + CreatedAt: query.FieldIdentifier[time.Time]{ + Field: "CreatedAt", + ModelType: universityType, + }, + DeletedAt: query.FieldIdentifier[time.Time]{ + Field: "DeletedAt", + ModelType: universityType, + }, + ID: query.FieldIdentifier[model.UUID]{ + Field: "ID", + ModelType: universityType, + }, + Name: query.FieldIdentifier[string]{ + Field: "Name", + ModelType: universityType, + }, + UpdatedAt: query.FieldIdentifier[time.Time]{ + Field: "UpdatedAt", + ModelType: universityType, + }, +} + +// Preload allows preloading the University when doing a query +func (universityConditions universityConditions) Preload() condition.Condition[models.University] { + return condition.NewPreloadCondition[models.University](universityConditions.ID, universityConditions.CreatedAt, universityConditions.UpdatedAt, universityConditions.DeletedAt, universityConditions.Name) +} diff --git a/testintegration/crudRepository.go b/testintegration/crudRepository.go new file mode 100644 index 00000000..a4657bf5 --- /dev/null +++ b/testintegration/crudRepository.go @@ -0,0 +1,64 @@ +package testintegration + +import ( + "github.com/stretchr/testify/suite" + "gorm.io/gorm" + "gotest.tools/assert" + + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/persistence/repository" + "github.com/ditrit/badaas/testintegration/models" +) + +type CRUDRepositoryIntTestSuite struct { + suite.Suite + db *gorm.DB + crudProductRepository repository.CRUD[models.Product, model.UUID] +} + +func NewCRUDRepositoryIntTestSuite( + db *gorm.DB, + crudProductRepository repository.CRUD[models.Product, model.UUID], +) *CRUDRepositoryIntTestSuite { + return &CRUDRepositoryIntTestSuite{ + db: db, + crudProductRepository: crudProductRepository, + } +} + +func (ts *CRUDRepositoryIntTestSuite) SetupTest() { + CleanDB(ts.db) +} + +func (ts *CRUDRepositoryIntTestSuite) TearDownSuite() { + CleanDB(ts.db) +} + +// ------------------------- GetByID -------------------------------- + +func (ts *CRUDRepositoryIntTestSuite) TestGetByIDReturnsErrorIfIDDontMatch() { + ts.createProduct(0) + _, err := ts.crudProductRepository.GetByID(ts.db, model.NilUUID) + ts.Error(err, gorm.ErrRecordNotFound) +} + +func (ts *CRUDRepositoryIntTestSuite) TestGetByIDReturnsEntityIfIDMatch() { + product := ts.createProduct(0) + ts.createProduct(0) + productReturned, err := ts.crudProductRepository.GetByID(ts.db, product.ID) + ts.Nil(err) + + assert.DeepEqual(ts.T(), product, productReturned) +} + +// ------------------------- utils ------------------------- + +func (ts *CRUDRepositoryIntTestSuite) createProduct(intV int) *models.Product { + entity := &models.Product{ + Int: intV, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} diff --git a/testintegration/db_models.go b/testintegration/db_models.go new file mode 100644 index 00000000..5e903b3e --- /dev/null +++ b/testintegration/db_models.go @@ -0,0 +1,52 @@ +package testintegration + +import ( + "log" + + "github.com/elliotchance/pie/v2" + "gorm.io/gorm" + + "github.com/ditrit/badaas/persistence/gormfx" + badaasModels "github.com/ditrit/badaas/persistence/models" + "github.com/ditrit/badaas/testintegration/models" +) + +var ListOfTables = []any{ + models.Product{}, + models.Company{}, + models.Seller{}, + models.Sale{}, + models.Country{}, + models.City{}, + models.Employee{}, + models.Person{}, + models.Bicycle{}, + models.Brand{}, + models.Phone{}, + models.ParentParent{}, + models.Parent1{}, + models.Parent2{}, + models.Child{}, + badaasModels.User{}, + badaasModels.Session{}, +} + +func GetModels() gormfx.GetModelsResult { + return gormfx.GetModelsResult{ + Models: ListOfTables, + } +} + +func CleanDB(db *gorm.DB) { + CleanDBTables(db, pie.Reverse(ListOfTables)) +} + +func CleanDBTables(db *gorm.DB, listOfTables []any) { + // clean database to ensure independency between tests + for _, table := range listOfTables { + err := db.Unscoped().Where("1 = 1").Delete(table).Error + if err != nil { + log.Fatalln("could not clean database: ", err) + } + } +} diff --git a/testintegration/int_test_config.yml b/testintegration/int_test_config.yml new file mode 100644 index 00000000..e188e00b --- /dev/null +++ b/testintegration/int_test_config.yml @@ -0,0 +1,13 @@ +database: + host: localhost + port: 5000 + sslmode: disable + username: badaas + password: badaas_password2023 + name: badaas_db + init: + retry: 10 + retryTime: 5 + +logger: + mode: dev \ No newline at end of file diff --git a/testintegration/join_conditions_test.go b/testintegration/join_conditions_test.go new file mode 100644 index 00000000..f9416032 --- /dev/null +++ b/testintegration/join_conditions_test.go @@ -0,0 +1,515 @@ +package testintegration + +import ( + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm" + "github.com/ditrit/badaas/orm/errors" + "github.com/ditrit/badaas/orm/unsafe" + "github.com/ditrit/badaas/testintegration/conditions" + "github.com/ditrit/badaas/testintegration/models" +) + +type JoinConditionsIntTestSuite struct { + ORMIntTestSuite +} + +func NewJoinConditionsIntTestSuite( + db *gorm.DB, +) *JoinConditionsIntTestSuite { + return &JoinConditionsIntTestSuite{ + ORMIntTestSuite: ORMIntTestSuite{ + db: db, + }, + } +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsUintBelongsTo() { + brand1 := ts.createBrand("google") + brand2 := ts.createBrand("apple") + + match := ts.createPhone("pixel", *brand1) + ts.createPhone("iphone", *brand2) + + entities, err := orm.NewQuery[models.Phone]( + ts.db, + conditions.Phone.Brand( + conditions.Brand.NameIs().Eq("google"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Phone{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsBelongsTo() { + product1 := ts.createProduct("", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + match := ts.createSale(0, product1, nil) + ts.createSale(0, product2, nil) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + conditions.Product.IntIs().Eq(1), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsAndFiltersTheMainEntity() { + product1 := ts.createProduct("", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + seller2 := ts.createSeller("agustin", nil) + + match := ts.createSale(1, product1, seller1) + ts.createSale(2, product2, seller2) + ts.createSale(2, product1, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.CodeIs().Eq(1), + conditions.Sale.Product( + conditions.Product.IntIs().Eq(1), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsHasOneOptional() { + product1 := ts.createProduct("", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + seller2 := ts.createSeller("agustin", nil) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Seller( + conditions.Seller.NameIs().Eq("franco"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsHasOneSelfReferential() { + boss1 := &models.Employee{ + Name: "Xavier", + } + boss2 := &models.Employee{ + Name: "Vincent", + } + + match := ts.createEmployee("franco", boss1) + ts.createEmployee("pierre", boss2) + + entities, err := orm.NewQuery[models.Employee]( + ts.db, + conditions.Employee.Boss( + conditions.Employee.NameIs().Eq("Xavier"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Employee{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsOneToOne() { + capital1 := models.City{ + Name: "Buenos Aires", + } + capital2 := models.City{ + Name: "Paris", + } + + ts.createCountry("Argentina", capital1) + ts.createCountry("France", capital2) + + entities, err := orm.NewQuery[models.City]( + ts.db, + conditions.City.Country( + conditions.Country.NameIs().Eq("Argentina"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.City{&capital1}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsOneToOneReversed() { + capital1 := models.City{ + Name: "Buenos Aires", + } + capital2 := models.City{ + Name: "Paris", + } + + country1 := ts.createCountry("Argentina", capital1) + ts.createCountry("France", capital2) + + entities, err := orm.NewQuery[models.Country]( + ts.db, + conditions.Country.Capital( + conditions.City.NameIs().Eq("Buenos Aires"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Country{country1}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsWithEntityThatDefinesTableName() { + person1 := models.Person{ + Name: "franco", + } + person2 := models.Person{ + Name: "xavier", + } + + match := ts.createBicycle("BMX", person1) + ts.createBicycle("Shimano", person2) + + entities, err := orm.NewQuery[models.Bicycle]( + ts.db, + conditions.Bicycle.Owner( + conditions.Person.NameIs().Eq("franco"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Bicycle{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsOnHasMany() { + company1 := ts.createCompany("ditrit") + company2 := ts.createCompany("orness") + + match := ts.createSeller("franco", company1) + ts.createSeller("agustin", company2) + + entities, err := orm.NewQuery[models.Seller]( + ts.db, + conditions.Seller.Company( + conditions.Company.NameIs().Eq("ditrit"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Seller{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsOnDifferentAttributes() { + product1 := ts.createProduct("match", 1, 0.0, false, nil) + product2 := ts.createProduct("match", 2, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + seller2 := ts.createSeller("agustin", nil) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + conditions.Product.IntIs().Eq(1), + conditions.Product.StringIs().Eq("match"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsAddsDeletedAtAutomatically() { + product1 := ts.createProduct("match", 1, 0.0, false, nil) + product2 := ts.createProduct("match", 2, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + seller2 := ts.createSeller("agustin", nil) + + ts.Nil(ts.db.Delete(product2).Error) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + conditions.Product.StringIs().Eq("match"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsOnDeletedAt() { + product1 := ts.createProduct("match", 1, 0.0, false, nil) + product2 := ts.createProduct("match", 2, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + seller2 := ts.createSeller("agustin", nil) + + ts.Nil(ts.db.Delete(product1).Error) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + conditions.Product.DeletedAtIs().Eq(product1.DeletedAt.Time), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsAndFiltersByNil() { + product1 := ts.createProduct("", 1, 0.0, false, nil) + intProduct2 := 2 + product2 := ts.createProduct("", 2, 0.0, false, &intProduct2) + + match := ts.createSale(0, product1, nil) + ts.createSale(0, product2, nil) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + conditions.Product.IntPointerIs().Null(), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsDifferentEntities() { + product1 := ts.createProduct("", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + seller2 := ts.createSeller("agustin", nil) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + ts.createSale(0, product1, seller2) + ts.createSale(0, product2, seller1) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + conditions.Product.IntIs().Eq(1), + ), + conditions.Sale.Seller( + conditions.Seller.NameIs().Eq("franco"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestConditionThatJoinsMultipleTimes() { + product1 := ts.createProduct("", 0, 0.0, false, nil) + product2 := ts.createProduct("", 0, 0.0, false, nil) + + company1 := ts.createCompany("ditrit") + company2 := ts.createCompany("orness") + + seller1 := ts.createSeller("franco", company1) + seller2 := ts.createSeller("agustin", company2) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Seller( + conditions.Seller.NameIs().Eq("franco"), + conditions.Seller.Company( + conditions.Company.NameIs().Eq("ditrit"), + ), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestJoinWithUnsafeCondition() { + product1 := ts.createProduct("", 0, 0.0, false, nil) + product2 := ts.createProduct("", 0, 0.0, false, nil) + + company1 := ts.createCompany("ditrit") + company2 := ts.createCompany("orness") + + seller1 := ts.createSeller("ditrit", company1) + seller2 := ts.createSeller("agustin", company2) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Seller( + conditions.Seller.Company( + unsafe.NewCondition[models.Company]("%s.name = Seller.name"), + ), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestJoinWithEmptyConnectionConditionMakesNothing() { + product1 := ts.createProduct("", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + match1 := ts.createSale(0, product1, nil) + match2 := ts.createSale(0, product2, nil) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + orm.And[models.Product](), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match1, match2}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestJoinWithEmptyContainerConditionReturnsError() { + _, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + orm.Not[models.Product](), + ), + ).Find() + ts.ErrorIs(err, errors.ErrEmptyConditions) + ts.ErrorContains(err, "connector: Not; model: models.Product") +} + +func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorOver2Tables() { + company1 := ts.createCompany("ditrit") + company2 := ts.createCompany("orness") + + seller1 := ts.createSeller("ditrit", company1) + ts.createSeller("agustin", company2) + + entities, err := orm.NewQuery[models.Seller]( + ts.db, + conditions.Seller.Company( + conditions.Company.NameIs().Dynamic().Eq(conditions.Seller.Name), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Seller{seller1}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorOver2TablesAtMoreLevel() { + product1 := ts.createProduct("", 0, 0.0, false, nil) + product2 := ts.createProduct("", 0, 0.0, false, nil) + + company1 := ts.createCompany("ditrit") + company2 := ts.createCompany("orness") + + seller1 := ts.createSeller("ditrit", company1) + seller2 := ts.createSeller("agustin", company2) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Seller( + conditions.Seller.Company( + conditions.Company.NameIs().Dynamic().Eq(conditions.Seller.Name), + ), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorWithNotJoinedModelReturnsError() { + _, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.IdIs().Dynamic().Eq(conditions.ParentParent.ID), + ).Find() + ts.ErrorIs(err, errors.ErrFieldModelNotConcerned) + ts.ErrorContains(err, "not concerned model: models.ParentParent; operator: Eq; model: models.Child, field: ID") +} + +func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithoutSelectJoinReturnsError() { + _, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + conditions.Child.IdIs().Dynamic().Eq(conditions.ParentParent.ID), + ).Find() + ts.ErrorIs(err, errors.ErrJoinMustBeSelected) + ts.ErrorContains(err, "joined multiple times model: models.ParentParent; operator: Eq; model: models.Child, field: ID") +} + +func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithSelectJoin() { + parentParent := &models.ParentParent{Name: "franco"} + parent1 := &models.Parent1{ParentParent: *parentParent} + parent2 := &models.Parent2{ParentParent: *parentParent} + child := &models.Child{Parent1: *parent1, Parent2: *parent2, Name: "franco"} + err := ts.db.Create(child).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + conditions.Child.NameIs().Dynamic().Eq(conditions.ParentParent.Name).SelectJoin(0, 0), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Child{child}, entities) +} + +func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithoutSelectJoinOnMultivalueOperatorReturnsError() { + _, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + conditions.Child.IdIs().Dynamic().Between( + conditions.ParentParent.ID, + conditions.ParentParent.ID, + ), + ).Find() + ts.ErrorIs(err, errors.ErrJoinMustBeSelected) + ts.ErrorContains(err, "joined multiple times model: models.ParentParent; operator: Between; model: models.Child, field: ID") +} diff --git a/testintegration/models/badaas-orm.go b/testintegration/models/badaas-orm.go new file mode 100644 index 00000000..4492c23b --- /dev/null +++ b/testintegration/models/badaas-orm.go @@ -0,0 +1,47 @@ +// Code generated by badaas-cli v0.0.0, DO NOT EDIT. +package models + +import preload "github.com/ditrit/badaas/orm/preload" + +func (m Bicycle) GetOwner() (*Person, error) { + return preload.VerifyStructLoaded[Person](&m.Owner) +} +func (m Child) GetParent1() (*Parent1, error) { + return preload.VerifyStructLoaded[Parent1](&m.Parent1) +} +func (m Child) GetParent2() (*Parent2, error) { + return preload.VerifyStructLoaded[Parent2](&m.Parent2) +} +func (m City) GetCountry() (*Country, error) { + return preload.VerifyPointerWithIDLoaded[Country](m.CountryID, m.Country) +} +func (m Company) GetSellers() ([]Seller, error) { + return preload.VerifyCollectionLoaded[Seller](m.Sellers) +} +func (m Country) GetCapital() (*City, error) { + return preload.VerifyStructLoaded[City](&m.Capital) +} +func (m Employee) GetBoss() (*Employee, error) { + return preload.VerifyPointerLoaded[Employee](m.BossID, m.Boss) +} +func (m Parent1) GetParentParent() (*ParentParent, error) { + return preload.VerifyStructLoaded[ParentParent](&m.ParentParent) +} +func (m Parent2) GetParentParent() (*ParentParent, error) { + return preload.VerifyStructLoaded[ParentParent](&m.ParentParent) +} +func (m Phone) GetBrand() (*Brand, error) { + return preload.VerifyStructLoaded[Brand](&m.Brand) +} +func (m Sale) GetProduct() (*Product, error) { + return preload.VerifyStructLoaded[Product](&m.Product) +} +func (m Sale) GetSeller() (*Seller, error) { + return preload.VerifyPointerLoaded[Seller](m.SellerID, m.Seller) +} +func (m Seller) GetCompany() (*Company, error) { + return preload.VerifyPointerLoaded[Company](m.CompanyID, m.Company) +} +func (m Seller) GetUniversity() (*University, error) { + return preload.VerifyPointerLoaded[University](m.UniversityID, m.University) +} diff --git a/testintegration/models/models.go b/testintegration/models/models.go new file mode 100644 index 00000000..40b25ed1 --- /dev/null +++ b/testintegration/models/models.go @@ -0,0 +1,253 @@ +package models + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "strings" + + "gorm.io/gorm" + "gorm.io/gorm/schema" + + "github.com/ditrit/badaas/orm/model" +) + +type Company struct { + model.UUIDModel + + Name string + Sellers *[]Seller // Company HasMany Sellers (Company 0..1 -> 0..* Seller) +} + +func (m Company) Equal(other Company) bool { + return m.ID == other.ID +} + +type MultiString []string + +func (s *MultiString) Scan(src interface{}) error { + switch typedSrc := src.(type) { + case string: + *s = strings.Split(typedSrc, ",") + return nil + case []byte: + str := string(typedSrc) + *s = strings.Split(str, ",") + + return nil + default: + return fmt.Errorf("failed to scan multistring field - source is not a string, is %T", src) + } +} + +func (s MultiString) Value() (driver.Value, error) { + if len(s) == 0 { + return nil, nil + } + + return strings.Join(s, ","), nil +} + +func (MultiString) GormDataType() string { + return "text" +} + +func (MultiString) GormDBDataType(db *gorm.DB, _ *schema.Field) string { + switch db.Dialector.Name() { + case "sqlserver": + return "varchar(255)" + default: + return "text" + } +} + +type ToBeEmbedded struct { + EmbeddedInt int +} + +type ToBeGormEmbedded struct { + Int int +} + +type Product struct { + model.UUIDModel + + String string `gorm:"column:string_something_else"` + Int int + IntPointer *int + Float float64 + NullFloat sql.NullFloat64 + Bool bool + NullBool sql.NullBool + ByteArray []byte + MultiString MultiString + ToBeEmbedded + GormEmbedded ToBeGormEmbedded `gorm:"embedded;embeddedPrefix:gorm_embedded_"` +} + +func (m Product) Equal(other Product) bool { + return m.ID == other.ID +} + +type University struct { + model.UUIDModel + + Name string +} + +func (m University) Equal(other University) bool { + return m.ID == other.ID +} + +type Seller struct { + model.UUIDModel + + Name string + Company *Company + CompanyID *model.UUID // Company HasMany Sellers (Company 0..1 -> 0..* Seller) + + University *University + UniversityID *model.UUID +} + +type Sale struct { + model.UUIDModel + + Code int + Description string + + // Sale belongsTo Product (Sale 0..* -> 1 Product) + Product Product + ProductID model.UUID + + // Sale belongsTo Seller (Sale 0..* -> 0..1 Seller) + Seller *Seller + SellerID *model.UUID +} + +func (m Sale) Equal(other Sale) bool { + return m.ID == other.ID +} + +func (m Seller) Equal(other Seller) bool { + return m.Name == other.Name +} + +type Country struct { + model.UUIDModel + + Name string + Capital City // Country HasOne City (Country 1 -> 1 City) +} + +type City struct { + model.UUIDModel + + Name string + Country *Country + CountryID model.UUID // Country HasOne City (Country 1 -> 1 City) +} + +func (m Country) Equal(other Country) bool { + return m.Name == other.Name +} + +func (m City) Equal(other City) bool { + return m.Name == other.Name +} + +type Person struct { + model.UUIDModel + + Name string `gorm:"unique;type:VARCHAR(255)"` +} + +func (m Person) TableName() string { + return "persons_and_more_name" +} + +type Bicycle struct { + model.UUIDModel + + Name string + // Bicycle BelongsTo Person (Bicycle 0..* -> 1 Person) + Owner Person `gorm:"references:Name;foreignKey:OwnerName"` + OwnerName string +} + +func (m Bicycle) Equal(other Bicycle) bool { + return m.Name == other.Name +} + +type Brand struct { + model.UIntModel + + Name string +} + +func (m Brand) Equal(other Brand) bool { + return m.Name == other.Name +} + +type Phone struct { + model.UIntModel + + Name string + // Phone belongsTo Brand (Phone 0..* -> 1 Brand) + Brand Brand + BrandID uint +} + +func (m Phone) Equal(other Phone) bool { + return m.Name == other.Name +} + +type ParentParent struct { + model.UUIDModel + + Name string + Number int +} + +func (m ParentParent) Equal(other ParentParent) bool { + return m.ID == other.ID +} + +type Parent1 struct { + model.UUIDModel + + ParentParent ParentParent + ParentParentID model.UUID +} + +func (m Parent1) Equal(other Parent1) bool { + return m.ID == other.ID +} + +type Parent2 struct { + model.UUIDModel + + ParentParent ParentParent + ParentParentID model.UUID +} + +func (m Parent2) Equal(other Parent2) bool { + return m.ID == other.ID +} + +type Child struct { + model.UUIDModel + + Name string + Number int + + Parent1 Parent1 + Parent1ID model.UUID + + Parent2 Parent2 + Parent2ID model.UUID +} + +func (m Child) Equal(other Child) bool { + return m.ID == other.ID +} diff --git a/testintegration/models/mysql.go b/testintegration/models/mysql.go new file mode 100644 index 00000000..843f524a --- /dev/null +++ b/testintegration/models/mysql.go @@ -0,0 +1,21 @@ +//go:build mysql +// +build mysql + +package models + +import ( + "github.com/ditrit/badaas/orm/model" +) + +type Employee struct { + model.UUIDModel + + Name string + // mysql needs OnDelete to work with self-referential fk + Boss *Employee `gorm:"constraint:OnDelete:SET NULL"` // Self-Referential Has One (Employee 0..* -> 0..1 Employee) + BossID *model.UUID +} + +func (m Employee) Equal(other Employee) bool { + return m.Name == other.Name +} diff --git a/testintegration/models/others.go b/testintegration/models/others.go new file mode 100644 index 00000000..d417fc6a --- /dev/null +++ b/testintegration/models/others.go @@ -0,0 +1,20 @@ +//go:build !mysql +// +build !mysql + +package models + +import ( + "github.com/ditrit/badaas/orm/model" +) + +type Employee struct { + model.UUIDModel + + Name string + Boss *Employee // Self-Referential Has One (Employee 0..* -> 0..1 Employee) + BossID *model.UUID +} + +func (m Employee) Equal(other Employee) bool { + return m.Name == other.Name +} diff --git a/testintegration/operators_test.go b/testintegration/operators_test.go new file mode 100644 index 00000000..267850b1 --- /dev/null +++ b/testintegration/operators_test.go @@ -0,0 +1,881 @@ +package testintegration + +import ( + "database/sql" + "log" + "strings" + + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm" + "github.com/ditrit/badaas/orm/mysql" + "github.com/ditrit/badaas/orm/operator" + "github.com/ditrit/badaas/orm/psql" + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/orm/sqlite" + "github.com/ditrit/badaas/testintegration/conditions" + "github.com/ditrit/badaas/testintegration/models" +) + +type OperatorsIntTestSuite struct { + ORMIntTestSuite +} + +func NewOperatorsIntTestSuite( + db *gorm.DB, +) *OperatorsIntTestSuite { + return &OperatorsIntTestSuite{ + ORMIntTestSuite: ORMIntTestSuite{ + db: db, + }, + } +} + +func (ts *OperatorsIntTestSuite) TestEqPointers() { + intMatch := 1 + match := ts.createProduct("match", 1, 0, false, &intMatch) + + intNotMatch := 2 + ts.createProduct("match", 3, 0, false, &intNotMatch) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntPointerIs().Eq(1), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestEqNullableType() { + match := ts.createProduct("match", 0, 0, false, nil) + match.NullFloat = sql.NullFloat64{Valid: true, Float64: 1.3} + err := ts.db.Save(match).Error + ts.Nil(err) + + notMatch1 := ts.createProduct("not_match", 3, 0, false, nil) + notMatch1.NullFloat = sql.NullFloat64{Valid: true, Float64: 1.2} + err = ts.db.Save(notMatch1).Error + ts.Nil(err) + + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullFloatIs().Eq(1.3), + ).Find() + + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestNotEq() { + match1 := ts.createProduct("match", 1, 0, false, nil) + match2 := ts.createProduct("match", 3, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().NotEq(2), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestLt() { + match1 := ts.createProduct("match", 1, 0, false, nil) + match2 := ts.createProduct("match", 2, 0, false, nil) + ts.createProduct("not_match", 3, 0, false, nil) + ts.createProduct("not_match", 4, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Lt(3), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestLtOrEq() { + match1 := ts.createProduct("match", 1, 0, false, nil) + match2 := ts.createProduct("match", 2, 0, false, nil) + ts.createProduct("not_match", 3, 0, false, nil) + ts.createProduct("not_match", 4, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().LtOrEq(2), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestGt() { + match1 := ts.createProduct("match", 3, 0, false, nil) + match2 := ts.createProduct("match", 4, 0, false, nil) + ts.createProduct("not_match", 1, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Gt(2), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestGtOrEq() { + match1 := ts.createProduct("match", 3, 0, false, nil) + match2 := ts.createProduct("match", 4, 0, false, nil) + ts.createProduct("not_match", 1, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().GtOrEq(3), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestBetween() { + match1 := ts.createProduct("match", 3, 0, false, nil) + match2 := ts.createProduct("match", 4, 0, false, nil) + ts.createProduct("not_match", 6, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Between(3, 5), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestNotBetween() { + match1 := ts.createProduct("match", 3, 0, false, nil) + match2 := ts.createProduct("match", 4, 0, false, nil) + ts.createProduct("not_match", 1, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().NotBetween(0, 2), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestIsNullPointers() { + match := ts.createProduct("match", 0, 0, false, nil) + int1 := 1 + int2 := 2 + + ts.createProduct("not_match", 0, 0, false, &int1) + ts.createProduct("not_match", 0, 0, false, &int2) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntPointerIs().Null(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestIsNullNullableTypes() { + match := ts.createProduct("match", 0, 0, false, nil) + + notMatch := ts.createProduct("not_match", 0, 0, false, nil) + notMatch.NullFloat = sql.NullFloat64{Valid: true, Float64: 6} + err := ts.db.Save(notMatch).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullFloatIs().Null(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestIsNotNullPointers() { + int1 := 1 + match := ts.createProduct("match", 0, 0, false, &int1) + ts.createProduct("not_match", 0, 0, false, nil) + ts.createProduct("not_match", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntPointerIs().NotNull(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestIsNotNullNullableTypes() { + match := ts.createProduct("match", 0, 0, false, nil) + match.NullFloat = sql.NullFloat64{Valid: true, Float64: 6} + err := ts.db.Save(match).Error + ts.Nil(err) + + ts.createProduct("not_match", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullFloatIs().NotNull(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestIsTrue() { + match := ts.createProduct("match", 0, 0, true, nil) + ts.createProduct("not_match", 0, 0, false, nil) + ts.createProduct("not_match", 0, 0, false, nil) + + var err error + + var entities []*models.Product + + switch getDBDialector() { + case query.Postgres, query.MySQL, query.SQLite: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.BoolIs().True(), + ).Find() + case query.SQLServer: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.BoolIs().Eq(true), + ).Find() + } + + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestIsFalse() { + match := ts.createProduct("match", 0, 0, false, nil) + ts.createProduct("not_match", 0, 0, true, nil) + ts.createProduct("not_match", 0, 0, true, nil) + + var err error + + var entities []*models.Product + + switch getDBDialector() { + case query.Postgres, query.MySQL, query.SQLite: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.BoolIs().False(), + ).Find() + case query.SQLServer: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.BoolIs().Eq(false), + ).Find() + } + + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +//nolint:dupl // not really duplicated +func (ts *OperatorsIntTestSuite) TestIsNotTrue() { + match1 := ts.createProduct("match", 0, 0, false, nil) + match2 := ts.createProduct("match", 0, 0, false, nil) + match2.NullBool = sql.NullBool{Valid: true, Bool: false} + err := ts.db.Save(match2).Error + ts.Nil(err) + + notMatch := ts.createProduct("not_match", 0, 0, false, nil) + notMatch.NullBool = sql.NullBool{Valid: true, Bool: true} + err = ts.db.Save(notMatch).Error + ts.Nil(err) + + var entities []*models.Product + + switch getDBDialector() { + case query.Postgres, query.MySQL, query.SQLite: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullBoolIs().NotTrue(), + ).Find() + case query.SQLServer: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullBoolIs().Distinct(true), + ).Find() + } + + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +//nolint:dupl // not really duplicated +func (ts *OperatorsIntTestSuite) TestIsNotFalse() { + match1 := ts.createProduct("match", 0, 0, false, nil) + match2 := ts.createProduct("match", 0, 0, false, nil) + match2.NullBool = sql.NullBool{Valid: true, Bool: true} + err := ts.db.Save(match2).Error + ts.Nil(err) + + notMatch := ts.createProduct("not_match", 0, 0, false, nil) + notMatch.NullBool = sql.NullBool{Valid: true, Bool: false} + err = ts.db.Save(notMatch).Error + ts.Nil(err) + + var entities []*models.Product + + switch getDBDialector() { + case query.Postgres, query.MySQL, query.SQLite: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullBoolIs().NotFalse(), + ).Find() + case query.SQLServer: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullBoolIs().Distinct(false), + ).Find() + } + + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestIsUnknown() { + match := ts.createProduct("match", 0, 0, false, nil) + + notMatch1 := ts.createProduct("match", 0, 0, false, nil) + notMatch1.NullBool = sql.NullBool{Valid: true, Bool: true} + err := ts.db.Save(notMatch1).Error + ts.Nil(err) + + notMatch2 := ts.createProduct("not_match", 0, 0, false, nil) + notMatch2.NullBool = sql.NullBool{Valid: true, Bool: false} + err = ts.db.Save(notMatch2).Error + ts.Nil(err) + + var entities []*models.Product + + switch getDBDialector() { + case query.Postgres, query.MySQL: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullBoolIs().Unknown(), + ).Find() + case query.SQLServer, query.SQLite: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullBoolIs().Null(), + ).Find() + } + + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestIsNotUnknown() { + match1 := ts.createProduct("", 0, 0, false, nil) + match1.NullBool = sql.NullBool{Valid: true, Bool: true} + err := ts.db.Save(match1).Error + ts.Nil(err) + + match2 := ts.createProduct("", 0, 0, false, nil) + match2.NullBool = sql.NullBool{Valid: true, Bool: false} + err = ts.db.Save(match2).Error + ts.Nil(err) + + ts.createProduct("", 0, 0, false, nil) + + var entities []*models.Product + + switch getDBDialector() { + case query.Postgres, query.MySQL: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullBoolIs().NotUnknown(), + ).Find() + case query.SQLServer, query.SQLite: + entities, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.NullBoolIs().NotNull(), + ).Find() + } + + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestIsDistinct() { + switch getDBDialector() { + case query.Postgres, query.SQLServer, query.SQLite: + match1 := ts.createProduct("match", 3, 0, false, nil) + match2 := ts.createProduct("match", 4, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Distinct(2), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) + case query.MySQL: + log.Println("IsDistinct not compatible") + } +} + +func (ts *OperatorsIntTestSuite) TestIsNotDistinct() { + switch getDBDialector() { + case query.Postgres, query.SQLServer, query.SQLite: + match := ts.createProduct("match", 3, 0, false, nil) + ts.createProduct("not_match", 4, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().NotDistinct(3), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) + case query.MySQL: + log.Println("IsNotDistinct not compatible") + } +} + +func (ts *OperatorsIntTestSuite) TestArrayIn() { + match1 := ts.createProduct("s1", 0, 0, false, nil) + match2 := ts.createProduct("s2", 0, 0, false, nil) + + ts.createProduct("ns1", 0, 0, false, nil) + ts.createProduct("ns2", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().In("s1", "s2", "s3"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestArrayNotIn() { + match1 := ts.createProduct("s1", 0, 0, false, nil) + match2 := ts.createProduct("s2", 0, 0, false, nil) + + ts.createProduct("ns1", 0, 0, false, nil) + ts.createProduct("ns2", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().NotIn("ns1", "ns2"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestLike() { + match1 := ts.createProduct("basd", 0, 0, false, nil) + match2 := ts.createProduct("cape", 0, 0, false, nil) + + ts.createProduct("bbsd", 0, 0, false, nil) + ts.createProduct("bbasd", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Like("_a%"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestLikeEscape() { + match1 := ts.createProduct("ba_sd", 0, 0, false, nil) + match2 := ts.createProduct("ca_pe", 0, 0, false, nil) + + ts.createProduct("bb_sd", 0, 0, false, nil) + ts.createProduct("bba_sd", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Custom( + operator.Like("_a!_%").Escape('!'), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *OperatorsIntTestSuite) TestLikeOnNumeric() { + switch getDBDialector() { + case query.Postgres, query.SQLServer, query.SQLite: + log.Println("Like with numeric not compatible") + case query.MySQL: + match1 := ts.createProduct("", 10, 0, false, nil) + match2 := ts.createProduct("", 100, 0, false, nil) + + ts.createProduct("", 20, 0, false, nil) + ts.createProduct("", 3, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Custom( + mysql.Like[int]("1%"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) + } +} + +func (ts *OperatorsIntTestSuite) TestILike() { + switch getDBDialector() { + case query.MySQL, query.SQLServer, query.SQLite: + log.Println("ILike not compatible") + case query.Postgres: + match1 := ts.createProduct("basd", 0, 0, false, nil) + match2 := ts.createProduct("cape", 0, 0, false, nil) + match3 := ts.createProduct("bAsd", 0, 0, false, nil) + + ts.createProduct("bbsd", 0, 0, false, nil) + ts.createProduct("bbasd", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Custom( + psql.ILike("_a%"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2, match3}, entities) + } +} + +func (ts *OperatorsIntTestSuite) TestSimilarTo() { + switch getDBDialector() { + case query.MySQL, query.SQLServer, query.SQLite: + log.Println("SimilarTo not compatible") + case query.Postgres: + match1 := ts.createProduct("abc", 0, 0, false, nil) + match2 := ts.createProduct("aabcc", 0, 0, false, nil) + + ts.createProduct("aec", 0, 0, false, nil) + ts.createProduct("aaaaa", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Custom( + psql.SimilarTo("%(b|d)%"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) + } +} + +func (ts *OperatorsIntTestSuite) TestPosixRegexCaseSensitive() { + match1 := ts.createProduct("ab", 0, 0, false, nil) + match2 := ts.createProduct("ax", 0, 0, false, nil) + + ts.createProduct("bb", 0, 0, false, nil) + ts.createProduct("cx", 0, 0, false, nil) + ts.createProduct("AB", 0, 0, false, nil) + + var posixRegexOperator operator.Operator[string] + + switch getDBDialector() { + case query.SQLServer, query.MySQL: + log.Println("PosixRegex not compatible") + case query.Postgres: + posixRegexOperator = psql.POSIXMatch("^a(b|x)") + case query.SQLite: + posixRegexOperator = sqlite.Glob("a[bx]") + } + + if posixRegexOperator != nil { + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Custom( + posixRegexOperator, + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) + } +} + +func (ts *OperatorsIntTestSuite) TestPosixRegexCaseInsensitive() { + match1 := ts.createProduct("ab", 0, 0, false, nil) + match2 := ts.createProduct("ax", 0, 0, false, nil) + match3 := ts.createProduct("AB", 0, 0, false, nil) + + ts.createProduct("bb", 0, 0, false, nil) + ts.createProduct("cx", 0, 0, false, nil) + + var posixRegexOperator operator.Operator[string] + + switch getDBDialector() { + case query.SQLServer, query.SQLite: + log.Println("PosixRegex Case Insensitive not compatible") + case query.MySQL: + posixRegexOperator = mysql.RegexP("^a(b|x)") + case query.Postgres: + posixRegexOperator = psql.POSIXIMatch("^a(b|x)") + } + + if posixRegexOperator != nil { + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Custom( + posixRegexOperator, + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2, match3}, entities) + } +} + +func (ts *OperatorsIntTestSuite) TestDynamicOperatorForBasicType() { + int1 := 1 + product1 := ts.createProduct("", 1, 0.0, false, &int1) + ts.createProduct("", 2, 0.0, false, &int1) + ts.createProduct("", 0, 0.0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Dynamic().Eq(conditions.Product.IntPointer), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{product1}, entities) +} + +func (ts *OperatorsIntTestSuite) TestDynamicOperatorForCustomType() { + match := ts.createProduct("salut,hola", 1, 0.0, false, nil) + match.MultiString = models.MultiString{"salut", "hola"} + err := ts.db.Save(match).Error + ts.Nil(err) + + ts.createProduct("salut,hola", 1, 0.0, false, nil) + ts.createProduct("hola", 1, 0.0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.MultiStringIs().Dynamic().Eq(conditions.Product.MultiString), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestDynamicOperatorForBaseModelAttribute() { + match := ts.createProduct("", 1, 0.0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.CreatedAtIs().Dynamic().Eq(conditions.Product.CreatedAt), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestDynamicOperatorForNotNullTypeCanBeComparedWithNullableType() { + match := ts.createProduct("", 1, 1.0, false, nil) + match.NullFloat = sql.NullFloat64{Valid: true, Float64: 1.0} + err := ts.db.Save(match).Error + ts.Nil(err) + + ts.createProduct("", 1, 0.0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Dynamic().Eq(conditions.Product.NullFloat), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestUnsafeOperatorInCaseTypesNotMatchConvertible() { + // comparisons between types are allowed when they are convertible + match := ts.createProduct("", 0, 2.1, false, nil) + ts.createProduct("", 0, 0, false, nil) + ts.createProduct("", 0, 2, false, nil) + ts.createProduct("", 0, 2.3, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq("2.1"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *OperatorsIntTestSuite) TestUnsafeOperatorInCaseTypesNotMatchNotConvertible() { + switch getDBDialector() { + case query.SQLite: + // comparisons between types are allowed and matches nothing if not convertible + ts.createProduct("", 0, 0, false, nil) + ts.createProduct("", 0, 2, false, nil) + ts.createProduct("", 0, 2.3, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq("not_convertible_to_float"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{}, entities) + case query.MySQL: + // comparisons between types are allowed but matches 0s if not convertible + match := ts.createProduct("", 0, 0, false, nil) + ts.createProduct("", 0, 2, false, nil) + ts.createProduct("", 0, 2.3, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq("not_convertible_to_float"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) + case query.SQLServer: + // returns an error + _, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq("not_convertible_to_float"), + ).Find() + ts.ErrorContains(err, "mssql: Error converting data type nvarchar to float.") + case query.Postgres: + // returns an error + _, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq("not_convertible_to_float"), + ).Find() + ts.ErrorContains(err, "not_convertible_to_float") + } +} + +func (ts *OperatorsIntTestSuite) TestUnsafeOperatorInCaseFieldWithTypesNotMatch() { + switch getDBDialector() { + case query.SQLite: + // comparisons between fields with different types are allowed + match1 := ts.createProduct("0", 0, 0, false, nil) + match2 := ts.createProduct("1", 0, 1, false, nil) + ts.createProduct("0", 0, 1, false, nil) + ts.createProduct("not_convertible", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq(conditions.Product.String), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) + case query.MySQL: + // comparisons between fields with different types are allowed but matches 0s on not convertible + match1 := ts.createProduct("0", 1, 0, false, nil) + match2 := ts.createProduct("1", 2, 1, false, nil) + match3 := ts.createProduct("not_convertible", 2, 0, false, nil) + ts.createProduct("0.0", 2, 1.0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq(conditions.Product.String), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2, match3}, entities) + case query.SQLServer: + // comparisons between fields with different types are allowed and returns error only if at least one is not convertible + match1 := ts.createProduct("0", 1, 0, false, nil) + match2 := ts.createProduct("1", 2, 1, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq(conditions.Product.String), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) + + ts.createProduct("not_convertible", 3, 0, false, nil) + ts.createProduct("0.0", 4, 1.0, false, nil) + + _, err = orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq(conditions.Product.String), + ).Find() + ts.ErrorContains(err, "mssql: Error converting data type nvarchar to float.") + case query.Postgres: + // returns an error + _, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Unsafe().Eq(conditions.Product.String), + ).Find() + + ts.True( + strings.Contains( + err.Error(), + "ERROR: operator does not exist: numeric = text (SQLSTATE 42883)", // postgresql + ) || strings.Contains( + err.Error(), + "ERROR: unsupported comparison operator: = (SQLSTATE 22023)", // cockroachdb + ), + ) + } +} + +func (ts *OperatorsIntTestSuite) TestUnsafeOperatorCanCompareFieldsThatMapToTheSameType() { + match := ts.createProduct("hola,chau", 1, 1.0, false, nil) + match.MultiString = models.MultiString{"hola", "chau"} + err := ts.db.Save(match).Error + ts.Nil(err) + + notMatch := ts.createProduct("chau", 0, 0.0, false, nil) + notMatch.MultiString = models.MultiString{"hola", "chau"} + err = ts.db.Save(notMatch).Error + ts.Nil(err) + + ts.createProduct("", 0, 0.0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Unsafe().Eq(conditions.Product.MultiString), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} diff --git a/testintegration/orm.go b/testintegration/orm.go new file mode 100644 index 00000000..85a26d02 --- /dev/null +++ b/testintegration/orm.go @@ -0,0 +1,138 @@ +package testintegration + +import ( + "github.com/stretchr/testify/suite" + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/testintegration/models" +) + +type ORMIntTestSuite struct { + suite.Suite + db *gorm.DB +} + +func (ts *ORMIntTestSuite) SetupTest() { + CleanDB(ts.db) +} + +func (ts *ORMIntTestSuite) TearDownSuite() { + CleanDB(ts.db) +} + +func (ts *ORMIntTestSuite) createProduct(stringV string, intV int, floatV float64, boolV bool, intP *int) *models.Product { + entity := &models.Product{ + String: stringV, + Int: intV, + Float: floatV, + Bool: boolV, + IntPointer: intP, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} + +func (ts *ORMIntTestSuite) createSale(code int, product *models.Product, seller *models.Seller) *models.Sale { + entity := &models.Sale{ + Code: code, + Product: *product, + Seller: seller, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} + +func (ts *ORMIntTestSuite) createSeller(name string, company *models.Company) *models.Seller { + var companyID *model.UUID + if company != nil { + companyID = &company.ID + } + + entity := &models.Seller{ + Name: name, + CompanyID: companyID, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} + +func (ts *ORMIntTestSuite) createCompany(name string) *models.Company { + entity := &models.Company{ + Name: name, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} + +func (ts *ORMIntTestSuite) createCountry(name string, capital models.City) *models.Country { + entity := &models.Country{ + Name: name, + Capital: capital, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} + +func (ts *ORMIntTestSuite) createEmployee(name string, boss *models.Employee) *models.Employee { + entity := &models.Employee{ + Name: name, + Boss: boss, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} + +func (ts *ORMIntTestSuite) createBicycle(name string, owner models.Person) *models.Bicycle { + entity := &models.Bicycle{ + Name: name, + Owner: owner, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} + +func (ts *ORMIntTestSuite) createBrand(name string) *models.Brand { + entity := &models.Brand{ + Name: name, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} + +func (ts *ORMIntTestSuite) createPhone(name string, brand models.Brand) *models.Phone { + entity := &models.Phone{ + Name: name, + Brand: brand, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} + +func (ts *ORMIntTestSuite) createUniversity(name string) *models.University { + entity := &models.University{ + Name: name, + } + err := ts.db.Create(entity).Error + ts.Nil(err) + + return entity +} diff --git a/testintegration/orm_test.go b/testintegration/orm_test.go new file mode 100644 index 00000000..eb03d97a --- /dev/null +++ b/testintegration/orm_test.go @@ -0,0 +1,100 @@ +package testintegration + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "go.uber.org/fx" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm" + "github.com/ditrit/badaas/orm/logger" + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/persistence/database" + "github.com/ditrit/badaas/persistence/gormfx" +) + +var tGlobal *testing.T + +const dbTypeEnvKey = "DB" + +const ( + username = "badaas" + password = "badaas_password2023" + host = "localhost" + port = 5000 + sslMode = "disable" + dbName = "badaas_db" +) + +func TestBaDaaSORM(t *testing.T) { + tGlobal = t + + fx.New( + // connect to db + fx.Provide(NewDBConnection), + fx.Provide(GetModels), + gormfx.AutoMigrate, + + // create test suites + fx.Provide(NewQueryIntTestSuite), + fx.Provide(NewWhereConditionsIntTestSuite), + fx.Provide(NewJoinConditionsIntTestSuite), + fx.Provide(NewPreloadConditionsIntTestSuite), + fx.Provide(NewOperatorsIntTestSuite), + + // run tests + fx.Invoke(runORMTestSuites), + ).Run() +} + +func runORMTestSuites( + tsQuery *QueryIntTestSuite, + tsWhereConditions *WhereConditionsIntTestSuite, + tsJoinConditions *JoinConditionsIntTestSuite, + tsPreloadConditions *PreloadConditionsIntTestSuite, + tsOperators *OperatorsIntTestSuite, + shutdowner fx.Shutdowner, +) { + suite.Run(tGlobal, tsQuery) + suite.Run(tGlobal, tsWhereConditions) + suite.Run(tGlobal, tsJoinConditions) + suite.Run(tGlobal, tsPreloadConditions) + suite.Run(tGlobal, tsOperators) + + shutdowner.Shutdown() +} + +func NewDBConnection() (*gorm.DB, error) { + var dialector gorm.Dialector + + switch getDBDialector() { + case query.Postgres: + dialector = postgres.Open(orm.CreatePostgreSQLDSN(host, username, password, sslMode, dbName, port)) + case query.SQLite: + dialector = sqlite.Open(orm.CreateSQLiteDSN(host)) + case query.MySQL: + dialector = mysql.Open(orm.CreateMySQLDSN(host, username, password, dbName, port)) + case query.SQLServer: + dialector = sqlserver.Open(orm.CreateSQLServerDSN(host, username, password, dbName, port)) + default: + return nil, fmt.Errorf("unknown db %s", getDBDialector()) + } + + return database.OpenWithRetry( + dialector, + logger.Default.ToLogMode(logger.Info), + 10, time.Duration(5)*time.Second, + ) +} + +func getDBDialector() query.Dialector { + return query.Dialector(os.Getenv(dbTypeEnvKey)) +} diff --git a/testintegration/preload_conditions_test.go b/testintegration/preload_conditions_test.go new file mode 100644 index 00000000..1842f239 --- /dev/null +++ b/testintegration/preload_conditions_test.go @@ -0,0 +1,891 @@ +package testintegration + +import ( + "errors" + + "github.com/elliotchance/pie/v2" + "gorm.io/gorm" + "gotest.tools/assert" + + "github.com/ditrit/badaas/orm" + badaasORMErrors "github.com/ditrit/badaas/orm/errors" + "github.com/ditrit/badaas/orm/model" + "github.com/ditrit/badaas/testintegration/conditions" + "github.com/ditrit/badaas/testintegration/models" + "github.com/ditrit/badaas/utils" +) + +type PreloadConditionsIntTestSuite struct { + ORMIntTestSuite +} + +func NewPreloadConditionsIntTestSuite( + db *gorm.DB, +) *PreloadConditionsIntTestSuite { + return &PreloadConditionsIntTestSuite{ + ORMIntTestSuite: ORMIntTestSuite{ + db: db, + }, + } +} + +func (ts *PreloadConditionsIntTestSuite) TestNoPreloadReturnsErrorOnGetRelation() { + product := ts.createProduct("a_string", 1, 0.0, false, nil) + seller := ts.createSeller("franco", nil) + sale := ts.createSale(0, product, seller) + + entities, err := orm.NewQuery[models.Sale](ts.db).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{sale}, entities) + + saleLoaded := entities[0] + + ts.False(saleLoaded.Product.IsLoaded()) + _, err = saleLoaded.GetProduct() + ts.ErrorIs(err, badaasORMErrors.ErrRelationNotLoaded) + + ts.Nil(saleLoaded.Seller) // is nil but we cant determine why directly (not loaded or really null) + _, err = saleLoaded.GetSeller() // GetSeller give us that information + ts.ErrorIs(err, badaasORMErrors.ErrRelationNotLoaded) +} + +func (ts *PreloadConditionsIntTestSuite) TestNoPreloadWhenItsNullKnowsItsReallyNull() { + product := ts.createProduct("a_string", 1, 0.0, false, nil) + sale := ts.createSale(10, product, nil) + + entities, err := orm.NewQuery[models.Sale](ts.db).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{sale}, entities) + + saleLoaded := entities[0] + + ts.False(saleLoaded.Product.IsLoaded()) + _, err = saleLoaded.GetProduct() + ts.ErrorIs(err, badaasORMErrors.ErrRelationNotLoaded) + + ts.Nil(saleLoaded.Seller) // is nil but we cant determine why directly (not loaded or really null) + saleSeller, err := saleLoaded.GetSeller() // GetSeller give us that information + ts.Nil(err) + ts.Nil(saleSeller) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadWithoutWhereConditionDoesNotFilter() { + product1 := ts.createProduct("a_string", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + + withSeller := ts.createSale(0, product1, seller1) + withoutSeller := ts.createSale(0, product2, nil) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.PreloadSeller(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{withSeller, withoutSeller}, entities) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + saleSeller, err := sale.GetSeller() + return err == nil && saleSeller != nil && saleSeller.Equal(*seller1) + })) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + return sale.Seller == nil + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadNullableAtSecondLevel() { + product1 := ts.createProduct("a_string", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + company := ts.createCompany("ditrit") + + withCompany := ts.createSeller("with", company) + withoutCompany := ts.createSeller("without", nil) + + sale1 := ts.createSale(0, product1, withoutCompany) + sale2 := ts.createSale(0, product2, withCompany) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Seller( + conditions.Seller.PreloadCompany(), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{sale1, sale2}, entities) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + saleSeller, err := sale.GetSeller() + if err != nil { + return false + } + + sellerCompany, err := saleSeller.GetCompany() + return err == nil && saleSeller.Name == "with" && sellerCompany != nil && sellerCompany.Equal(*company) + })) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + saleSeller, err := sale.GetSeller() + if err != nil { + return false + } + + sellerCompany, err := saleSeller.GetCompany() + return err == nil && saleSeller.Name == "without" && sellerCompany == nil + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadAtSecondLevelWorksWithManualPreload() { + product1 := ts.createProduct("a_string", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + company := ts.createCompany("ditrit") + + withCompany := ts.createSeller("with", company) + withoutCompany := ts.createSeller("without", nil) + + sale1 := ts.createSale(0, product1, withoutCompany) + sale2 := ts.createSale(0, product2, withCompany) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Seller( + conditions.Seller.Preload(), + conditions.Seller.PreloadCompany(), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{sale1, sale2}, entities) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + saleSeller, err := sale.GetSeller() + if err != nil { + return false + } + + sellerCompany, err := saleSeller.GetCompany() + return err == nil && saleSeller.Name == "with" && sellerCompany != nil && sellerCompany.Equal(*company) + })) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + saleSeller, err := sale.GetSeller() + if err != nil { + return false + } + + sellerCompany, err := saleSeller.GetCompany() + return err == nil && saleSeller.Name == "without" && sellerCompany == nil + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestNoPreloadNullableAtSecondLevel() { + product1 := ts.createProduct("a_string", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + company := ts.createCompany("ditrit") + + withCompany := ts.createSeller("with", company) + withoutCompany := ts.createSeller("without", nil) + + sale1 := ts.createSale(0, product1, withoutCompany) + sale2 := ts.createSale(0, product2, withCompany) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.PreloadSeller(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{sale1, sale2}, entities) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + saleSeller, err := sale.GetSeller() + if err != nil { + return false + } + + // the not null one is not loaded + sellerCompany, err := saleSeller.GetCompany() + return errors.Is(err, badaasORMErrors.ErrRelationNotLoaded) && sellerCompany == nil + })) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + saleSeller, err := sale.GetSeller() + if err != nil { + return false + } + + // we can be sure the null one is null + sellerCompany, err := saleSeller.GetCompany() + return err == nil && sellerCompany == nil + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadWithoutWhereConditionDoesNotFilterAtSecondLevel() { + product1 := ts.createProduct("a_string", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + + withSeller := ts.createSale(0, product1, seller1) + withoutSeller := ts.createSale(0, product2, nil) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Seller( + conditions.Seller.PreloadCompany(), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{withSeller, withoutSeller}, entities) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + saleSeller, err := sale.GetSeller() + if saleSeller == nil || err != nil { + return false + } + + sellerCompany, err := saleSeller.GetCompany() + + return err == nil && saleSeller.Equal(*seller1) && sellerCompany == nil + })) + ts.True(pie.Any(entities, func(sale *models.Sale) bool { + // in this case sale.Seller will also be nil + // but we can now it's really null in the db because err is nil + saleSeller, err := sale.GetSeller() + return err == nil && saleSeller == nil + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadUIntModel() { + brand1 := ts.createBrand("google") + brand2 := ts.createBrand("apple") + + phone1 := ts.createPhone("pixel", *brand1) + phone2 := ts.createPhone("iphone", *brand2) + + entities, err := orm.NewQuery[models.Phone]( + ts.db, + conditions.Phone.PreloadBrand(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Phone{phone1, phone2}, entities) + ts.True(pie.Any(entities, func(phone *models.Phone) bool { + phoneBrand, err := phone.GetBrand() + return err == nil && phoneBrand.Equal(*brand1) + })) + ts.True(pie.Any(entities, func(phone *models.Phone) bool { + phoneBrand, err := phone.GetBrand() + return err == nil && phoneBrand.Equal(*brand2) + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadWithWhereConditionFilters() { + product1 := ts.createProduct("a_string", 1, 0.0, false, nil) + product1.EmbeddedInt = 1 + product1.GormEmbedded.Int = 2 + err := ts.db.Save(product1).Error + ts.Nil(err) + + product2 := ts.createProduct("", 2, 0.0, false, nil) + + match := ts.createSale(0, product1, nil) + ts.createSale(0, product2, nil) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + conditions.Product.Preload(), + conditions.Product.IntIs().Eq(1), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) + saleProduct, err := entities[0].GetProduct() + ts.Nil(err) + assert.DeepEqual(ts.T(), product1, saleProduct) + ts.Equal("a_string", saleProduct.String) + ts.Equal(1, saleProduct.EmbeddedInt) + ts.Equal(2, saleProduct.GormEmbedded.Int) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadOneToOne() { + capital1 := models.City{ + Name: "Buenos Aires", + } + capital2 := models.City{ + Name: "Paris", + } + + country1 := ts.createCountry("Argentina", capital1) + country2 := ts.createCountry("France", capital2) + + entities, err := orm.NewQuery[models.City]( + ts.db, + conditions.City.PreloadCountry(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.City{&capital1, &capital2}, entities) + ts.True(pie.Any(entities, func(city *models.City) bool { + cityCountry, err := city.GetCountry() + if err != nil { + return false + } + + return cityCountry.Equal(*country1) + })) + ts.True(pie.Any(entities, func(city *models.City) bool { + cityCountry, err := city.GetCountry() + if err != nil { + return false + } + + return cityCountry.Equal(*country2) + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestNoPreloadOneToOne() { + capital1 := models.City{ + Name: "Buenos Aires", + } + + ts.createCountry("Argentina", capital1) + + entities, err := orm.NewQuery[models.City](ts.db).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.City{&capital1}, entities) + _, err = entities[0].GetCountry() + ts.ErrorIs(err, badaasORMErrors.ErrRelationNotLoaded) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadOneToOneReversed() { + capital1 := models.City{ + Name: "Buenos Aires", + } + capital2 := models.City{ + Name: "Paris", + } + + country1 := ts.createCountry("Argentina", capital1) + country2 := ts.createCountry("France", capital2) + + entities, err := orm.NewQuery[models.Country]( + ts.db, + conditions.Country.PreloadCapital(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Country{country1, country2}, entities) + ts.True(pie.Any(entities, func(country *models.Country) bool { + countryCapital, err := country.GetCapital() + return err == nil && countryCapital.Equal(capital1) + })) + ts.True(pie.Any(entities, func(country *models.Country) bool { + countryCapital, err := country.GetCapital() + return err == nil && countryCapital.Equal(capital2) + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadHasManyReversed() { + company1 := ts.createCompany("ditrit") + company2 := ts.createCompany("orness") + + seller1 := ts.createSeller("franco", company1) + seller2 := ts.createSeller("agustin", company2) + + entities, err := orm.NewQuery[models.Seller]( + ts.db, + conditions.Seller.PreloadCompany(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Seller{seller1, seller2}, entities) + ts.True(pie.Any(entities, func(seller *models.Seller) bool { + sellerCompany, err := seller.GetCompany() + return err == nil && sellerCompany.Equal(*company1) + })) + ts.True(pie.Any(entities, func(seller *models.Seller) bool { + sellerCompany, err := seller.GetCompany() + return err == nil && sellerCompany.Equal(*company2) + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadSelfReferential() { + boss1 := &models.Employee{ + Name: "Xavier", + } + + employee1 := ts.createEmployee("franco", boss1) + employee2 := ts.createEmployee("pierre", nil) + + entities, err := orm.NewQuery[models.Employee]( + ts.db, + conditions.Employee.PreloadBoss(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Employee{boss1, employee1, employee2}, entities) + + ts.True(pie.Any(entities, func(employee *models.Employee) bool { + employeeBoss, err := employee.GetBoss() + return err == nil && employeeBoss != nil && employeeBoss.Equal(*boss1) + })) + ts.True(pie.Any(entities, func(employee *models.Employee) bool { + employeeBoss, err := employee.GetBoss() + return err == nil && employeeBoss == nil + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadSelfReferentialAtSecondLevel() { + bossBoss := &models.Employee{ + Name: "Xavier", + } + boss := &models.Employee{ + Name: "Vincent", + Boss: bossBoss, + } + employee := ts.createEmployee("franco", boss) + + entities, err := orm.NewQuery[models.Employee]( + ts.db, + conditions.Employee.Boss( + conditions.Employee.Boss( + conditions.Employee.Preload(), + ), + ), + conditions.Employee.NameIs().Eq("franco"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Employee{employee}, entities) + + bossLoaded, err := entities[0].GetBoss() + ts.Nil(err) + ts.True(bossLoaded.Equal(*boss)) + + bossBossLoaded, err := bossLoaded.GetBoss() + ts.Nil(err) + ts.True(bossBossLoaded.Equal(*bossBoss)) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadDifferentEntitiesWithConditions() { + product1 := ts.createProduct("", 1, 0.0, false, nil) + product2 := ts.createProduct("", 2, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + seller2 := ts.createSeller("agustin", nil) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + ts.createSale(0, product1, seller2) + ts.createSale(0, product2, seller1) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product( + conditions.Product.Preload(), + conditions.Product.IntIs().Eq(1), + ), + conditions.Sale.Seller( + conditions.Seller.Preload(), + conditions.Seller.NameIs().Eq("franco"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) + saleProduct, err := entities[0].GetProduct() + ts.Nil(err) + assert.DeepEqual(ts.T(), product1, saleProduct) + + saleSeller, err := entities[0].GetSeller() + ts.Nil(err) + assert.DeepEqual(ts.T(), seller1, saleSeller) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadDifferentEntitiesWithoutConditions() { + parentParent := &models.ParentParent{} + err := ts.db.Create(parentParent).Error + ts.Nil(err) + + parent1 := &models.Parent1{ParentParent: *parentParent} + err = ts.db.Create(parent1).Error + ts.Nil(err) + + parent2 := &models.Parent2{ParentParent: *parentParent} + err = ts.db.Create(parent2).Error + ts.Nil(err) + + child := &models.Child{Parent1: *parent1, Parent2: *parent2} + err = ts.db.Create(child).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.PreloadParent1(), + conditions.Child.PreloadParent2(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Child{child}, entities) + childParent1, err := entities[0].GetParent1() + ts.Nil(err) + assert.DeepEqual(ts.T(), parent1, childParent1) + + childParent2, err := entities[0].GetParent2() + ts.Nil(err) + assert.DeepEqual(ts.T(), parent2, childParent2) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadRelations() { + parentParent := &models.ParentParent{} + err := ts.db.Create(parentParent).Error + ts.Nil(err) + + parent1 := &models.Parent1{ParentParent: *parentParent} + err = ts.db.Create(parent1).Error + ts.Nil(err) + + parent2 := &models.Parent2{ParentParent: *parentParent} + err = ts.db.Create(parent2).Error + ts.Nil(err) + + child := &models.Child{Parent1: *parent1, Parent2: *parent2} + err = ts.db.Create(child).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.PreloadRelations()..., + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Child{child}, entities) + childParent1, err := entities[0].GetParent1() + ts.Nil(err) + assert.DeepEqual(ts.T(), parent1, childParent1) + + childParent2, err := entities[0].GetParent2() + ts.Nil(err) + assert.DeepEqual(ts.T(), parent2, childParent2) +} + +func (ts *PreloadConditionsIntTestSuite) TestJoinMultipleTimesAndPreloadWithoutCondition() { + parentParent := &models.ParentParent{} + err := ts.db.Create(parentParent).Error + ts.Nil(err) + + parent1 := &models.Parent1{ParentParent: *parentParent} + err = ts.db.Create(parent1).Error + ts.Nil(err) + + parent2 := &models.Parent2{ParentParent: *parentParent} + err = ts.db.Create(parent2).Error + ts.Nil(err) + + child := &models.Child{Parent1: *parent1, Parent2: *parent2} + err = ts.db.Create(child).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.Preload(), + conditions.Parent1.PreloadParentParent(), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Child{child}, entities) + childParent1, err := entities[0].GetParent1() + ts.Nil(err) + assert.DeepEqual(ts.T(), parent1, childParent1) + + childParentParent, err := childParent1.GetParentParent() + ts.Nil(err) + assert.DeepEqual(ts.T(), parentParent, childParentParent) +} + +func (ts *PreloadConditionsIntTestSuite) TestJoinMultipleTimesAndPreloadWithCondition() { + parentParent1 := &models.ParentParent{ + Name: "parentParent1", + } + err := ts.db.Create(parentParent1).Error + ts.Nil(err) + + parent11 := &models.Parent1{ParentParent: *parentParent1} + err = ts.db.Create(parent11).Error + ts.Nil(err) + + parent21 := &models.Parent2{ParentParent: *parentParent1} + err = ts.db.Create(parent21).Error + ts.Nil(err) + + child1 := &models.Child{Parent1: *parent11, Parent2: *parent21} + err = ts.db.Create(child1).Error + ts.Nil(err) + + parentParent2 := &models.ParentParent{} + err = ts.db.Create(parentParent2).Error + ts.Nil(err) + + parent12 := &models.Parent1{ParentParent: *parentParent2} + err = ts.db.Create(parent12).Error + ts.Nil(err) + + parent22 := &models.Parent2{ParentParent: *parentParent2} + err = ts.db.Create(parent22).Error + ts.Nil(err) + + child2 := &models.Child{Parent1: *parent12, Parent2: *parent22} + err = ts.db.Create(child2).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.Preload(), + conditions.Parent1.ParentParent( + conditions.ParentParent.Preload(), + conditions.ParentParent.NameIs().Eq("parentParent1"), + ), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Child{child1}, entities) + childParent1, err := entities[0].GetParent1() + ts.Nil(err) + assert.DeepEqual(ts.T(), parent11, childParent1) + + childParentParent, err := childParent1.GetParentParent() + ts.Nil(err) + assert.DeepEqual(ts.T(), parentParent1, childParentParent) +} + +func (ts *PreloadConditionsIntTestSuite) TestJoinMultipleTimesAndPreloadDiamond() { + parentParent := &models.ParentParent{} + err := ts.db.Create(parentParent).Error + ts.Nil(err) + + parent1 := &models.Parent1{ParentParent: *parentParent} + err = ts.db.Create(parent1).Error + ts.Nil(err) + + parent2 := &models.Parent2{ParentParent: *parentParent} + err = ts.db.Create(parent2).Error + ts.Nil(err) + + child := &models.Child{Parent1: *parent1, Parent2: *parent2} + err = ts.db.Create(child).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.PreloadParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.PreloadParentParent(), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Child{child}, entities) + childParent1, err := entities[0].GetParent1() + ts.Nil(err) + assert.DeepEqual(ts.T(), parent1, childParent1) + + childParent2, err := entities[0].GetParent2() + ts.Nil(err) + assert.DeepEqual(ts.T(), parent2, childParent2) + + childParent1Parent, err := childParent1.GetParentParent() + ts.Nil(err) + assert.DeepEqual(ts.T(), parentParent, childParent1Parent) + + childParent2Parent, err := childParent2.GetParentParent() + ts.Nil(err) + assert.DeepEqual(ts.T(), parentParent, childParent2Parent) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadCollection() { + company := ts.createCompany("ditrit") + seller1 := ts.createSeller("1", company) + seller2 := ts.createSeller("2", company) + + entities, err := orm.NewQuery[models.Company]( + ts.db, + conditions.Company.PreloadSellers(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Company{company}, entities) + companySellers, err := entities[0].GetSellers() + ts.Nil(err) + EqualList(&ts.Suite, []models.Seller{*seller1, *seller2}, companySellers) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadEmptyCollection() { + company := ts.createCompany("ditrit") + + entities, err := orm.NewQuery[models.Company]( + ts.db, + conditions.Company.PreloadSellers(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Company{company}, entities) + companySellers, err := entities[0].GetSellers() + ts.Nil(err) + EqualList(&ts.Suite, []models.Seller{}, companySellers) +} + +func (ts *PreloadConditionsIntTestSuite) TestNoPreloadCollection() { + company := ts.createCompany("ditrit") + + entities, err := orm.NewQuery[models.Company](ts.db).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Company{company}, entities) + _, err = entities[0].GetSellers() + ts.ErrorIs(err, badaasORMErrors.ErrRelationNotLoaded) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadListAndNestedAttributes() { + company := ts.createCompany("ditrit") + + university1 := ts.createUniversity("uni1") + seller1 := ts.createSeller("1", company) + seller1.University = university1 + err := ts.db.Save(seller1).Error + ts.Nil(err) + + university2 := ts.createUniversity("uni2") + seller2 := ts.createSeller("2", company) + seller2.University = university2 + err = ts.db.Save(seller2).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Company]( + ts.db, + conditions.Company.PreloadSellers( + conditions.Seller.PreloadUniversity(), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Company{company}, entities) + companySellers, err := entities[0].GetSellers() + ts.Nil(err) + EqualList(&ts.Suite, []models.Seller{*seller1, *seller2}, companySellers) + + ts.True(pie.Any(*entities[0].Sellers, func(seller models.Seller) bool { + sellerUniversity, err := seller.GetUniversity() + return err == nil && sellerUniversity.Equal(*university1) + })) + ts.True(pie.Any(*entities[0].Sellers, func(seller models.Seller) bool { + sellerUniversity, err := seller.GetUniversity() + return err == nil && sellerUniversity.Equal(*university2) + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadMultipleListsAndNestedAttributes() { + company1 := ts.createCompany("ditrit") + company2 := ts.createCompany("orness") + + university1 := ts.createUniversity("uni1") + seller1 := ts.createSeller("1", company1) + seller1.University = university1 + err := ts.db.Save(seller1).Error + ts.Nil(err) + + university2 := ts.createUniversity("uni2") + seller2 := ts.createSeller("2", company1) + seller2.University = university2 + err = ts.db.Save(seller2).Error + ts.Nil(err) + + seller3 := ts.createSeller("3", company2) + seller3.University = university1 + err = ts.db.Save(seller3).Error + ts.Nil(err) + + seller4 := ts.createSeller("4", company2) + seller4.University = university2 + err = ts.db.Save(seller4).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Company]( + ts.db, + conditions.Company.PreloadSellers( + conditions.Seller.PreloadUniversity(), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Company{company1, company2}, entities) + + company1Loaded := *utils.FindFirst(entities, func(company *models.Company) bool { + return company.Equal(*company1) + }) + company2Loaded := *utils.FindFirst(entities, func(company *models.Company) bool { + return company.Equal(*company2) + }) + + company1Sellers, err := company1Loaded.GetSellers() + ts.Nil(err) + EqualList(&ts.Suite, []models.Seller{*seller1, *seller2}, company1Sellers) + + var sellerUniversity *models.University + + ts.True(pie.Any(*company1Loaded.Sellers, func(seller models.Seller) bool { + sellerUniversity, err = seller.GetUniversity() + return err == nil && sellerUniversity.Equal(*university1) + })) + ts.True(pie.Any(*company1Loaded.Sellers, func(seller models.Seller) bool { + sellerUniversity, err = seller.GetUniversity() + return err == nil && sellerUniversity.Equal(*university2) + })) + + company2Sellers, err := company2Loaded.GetSellers() + ts.Nil(err) + EqualList(&ts.Suite, []models.Seller{*seller3, *seller4}, company2Sellers) + + ts.True(pie.Any(*company2Loaded.Sellers, func(seller models.Seller) bool { + sellerUniversity, err := seller.GetUniversity() + return err == nil && sellerUniversity.Equal(*university1) + })) + ts.True(pie.Any(*company2Loaded.Sellers, func(seller models.Seller) bool { + sellerUniversity, err := seller.GetUniversity() + return err == nil && sellerUniversity.Equal(*university2) + })) +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadListAndNestedAttributesWithFiltersReturnsError() { + _, err := orm.NewQuery[models.Company]( + ts.db, + conditions.Company.PreloadSellers( + conditions.Seller.University( + conditions.University.Preload(), + conditions.University.IdIs().Eq(model.NilUUID), + ), + ), + ).Find() + ts.ErrorIs(err, badaasORMErrors.ErrOnlyPreloadsAllowed) + ts.ErrorContains(err, "model: models.Company, field: Sellers") +} + +func (ts *PreloadConditionsIntTestSuite) TestPreloadListAndNestedAttributesWithoutPreloadReturnsError() { + _, err := orm.NewQuery[models.Company]( + ts.db, + conditions.Company.PreloadSellers( + conditions.Seller.University(), + ), + ).Find() + ts.ErrorIs(err, badaasORMErrors.ErrOnlyPreloadsAllowed) + ts.ErrorContains(err, "model: models.Company, field: Sellers") +} diff --git a/testintegration/query_test.go b/testintegration/query_test.go new file mode 100644 index 00000000..2d52abcd --- /dev/null +++ b/testintegration/query_test.go @@ -0,0 +1,330 @@ +package testintegration + +import ( + "github.com/google/go-cmp/cmp" + "gorm.io/gorm" + "gotest.tools/assert" + + "github.com/ditrit/badaas/orm" + "github.com/ditrit/badaas/orm/errors" + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/testintegration/conditions" + "github.com/ditrit/badaas/testintegration/models" +) + +type QueryIntTestSuite struct { + ORMIntTestSuite +} + +func NewQueryIntTestSuite( + db *gorm.DB, +) *QueryIntTestSuite { + return &QueryIntTestSuite{ + ORMIntTestSuite: ORMIntTestSuite{ + db: db, + }, + } +} + +func (ts *QueryIntTestSuite) SetupTest() { + CleanDB(ts.db) +} + +func (ts *QueryIntTestSuite) TearDownSuite() { + CleanDB(ts.db) +} + +// ------------------------- FindOne -------------------------------- + +func (ts *QueryIntTestSuite) TestFindOneReturnsErrorIfConditionsDontMatch() { + ts.createProduct("", 0, 0, false, nil) + _, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).FindOne() + ts.Error(err, gorm.ErrRecordNotFound) +} + +func (ts *QueryIntTestSuite) TestFindOneReturnsEntityIfConditionsMatch() { + product := ts.createProduct("", 1, 0, false, nil) + productReturned, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).FindOne() + ts.Nil(err) + + assert.DeepEqual(ts.T(), product, productReturned) +} + +func (ts *QueryIntTestSuite) TestFindOneReturnsErrorIfMoreThanOneMatchConditions() { + ts.createProduct("", 0, 0, false, nil) + ts.createProduct("", 0, 0, false, nil) + _, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(0), + ).FindOne() + ts.Error(err, errors.ErrMoreThanOneObjectFound) +} + +// ------------------------- First -------------------------------- + +func (ts *QueryIntTestSuite) TestFirstReturnsErrorIfConditionsDontMatch() { + ts.createProduct("", 0, 0, false, nil) + _, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).First() + ts.Error(err, gorm.ErrRecordNotFound) +} + +func (ts *QueryIntTestSuite) TestFirstReturnsFirstEntityIfConditionsMatch() { + brand1 := ts.createBrand("a") + ts.createBrand("a") + + brandReturned, err := orm.NewQuery[models.Brand]( + ts.db, + conditions.Brand.NameIs().Eq("a"), + ).First() + ts.Nil(err) + + assert.DeepEqual(ts.T(), brand1, brandReturned) +} + +// ------------------------- Last -------------------------------- + +func (ts *QueryIntTestSuite) TestLastReturnsErrorIfConditionsDontMatch() { + ts.createProduct("", 0, 0, false, nil) + _, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Last() + ts.Error(err, gorm.ErrRecordNotFound) +} + +func (ts *QueryIntTestSuite) TestLastReturnsLastEntityIfConditionsMatch() { + ts.createBrand("a") + brand2 := ts.createBrand("a") + + brandReturned, err := orm.NewQuery[models.Brand]( + ts.db, + conditions.Brand.NameIs().Eq("a"), + ).Last() + ts.Nil(err) + + assert.DeepEqual(ts.T(), brand2, brandReturned) +} + +// ------------------------- Take -------------------------------- + +func (ts *QueryIntTestSuite) TestTakeReturnsErrorIfConditionsDontMatch() { + ts.createProduct("", 0, 0, false, nil) + _, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Take() + ts.Error(err, gorm.ErrRecordNotFound) +} + +func (ts *QueryIntTestSuite) TestTakeReturnsFirstCreatedEntityIfConditionsMatch() { + product1 := ts.createProduct("", 1, 0, false, nil) + product2 := ts.createProduct("", 1, 0, false, nil) + productReturned, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Take() + ts.Nil(err) + + ts.True(cmp.Equal(productReturned, product1) || cmp.Equal(productReturned, product2)) +} + +// ------------------------- Order -------------------------------- + +func (ts *QueryIntTestSuite) TestAscendingReturnsResultsInAscendingOrder() { + product1 := ts.createProduct("", 1, 1.0, false, nil) + product2 := ts.createProduct("", 1, 2.0, false, nil) + products, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Ascending(conditions.Product.Float).Find() + ts.Nil(err) + + ts.Len(products, 2) + assert.DeepEqual(ts.T(), product1, products[0]) + assert.DeepEqual(ts.T(), product2, products[1]) +} + +func (ts *QueryIntTestSuite) TestDescendingReturnsResultsInDescendingOrder() { + product1 := ts.createProduct("", 1, 1.0, false, nil) + product2 := ts.createProduct("", 1, 2.0, false, nil) + products, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Descending(conditions.Product.Float).Find() + ts.Nil(err) + + ts.Len(products, 2) + assert.DeepEqual(ts.T(), product2, products[0]) + assert.DeepEqual(ts.T(), product1, products[1]) +} + +func (ts *QueryIntTestSuite) TestOrderByFieldThatIsJoined() { + product1 := ts.createProduct("", 0, 1.0, false, nil) + product2 := ts.createProduct("", 0, 2.0, false, nil) + + sale1 := ts.createSale(0, product1, nil) + sale2 := ts.createSale(0, product2, nil) + + sales, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.Product(), + ).Descending(conditions.Product.Float).Find() + ts.Nil(err) + + ts.Len(sales, 2) + assert.DeepEqual(ts.T(), sale2, sales[0]) + assert.DeepEqual(ts.T(), sale1, sales[1]) +} + +func (ts *QueryIntTestSuite) TestOrderReturnsErrorIfFieldIsNotConcerned() { + _, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Descending(conditions.Seller.ID).Find() + ts.ErrorIs(err, errors.ErrFieldModelNotConcerned) + ts.ErrorContains(err, "not concerned model: models.Seller; method: Descending") +} + +func (ts *QueryIntTestSuite) TestOrderReturnsErrorIfFieldIsJoinedMoreThanOnceAndJoinIsNotSelected() { + _, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Descending(conditions.ParentParent.ID).Find() + ts.ErrorIs(err, errors.ErrJoinMustBeSelected) + ts.ErrorContains(err, "joined multiple times model: models.ParentParent; method: Descending") +} + +func (ts *QueryIntTestSuite) TestOrderWorksIfFieldIsJoinedMoreThanOnceAndJoinIsSelected() { + parentParent1 := &models.ParentParent{Name: "a"} + parent11 := &models.Parent1{ParentParent: *parentParent1} + parent12 := &models.Parent2{ParentParent: *parentParent1} + child1 := &models.Child{Parent1: *parent11, Parent2: *parent12, Name: "franco"} + err := ts.db.Create(child1).Error + ts.Nil(err) + + parentParent2 := &models.ParentParent{Name: "b"} + parent21 := &models.Parent1{ParentParent: *parentParent2} + parent22 := &models.Parent2{ParentParent: *parentParent2} + child2 := &models.Child{Parent1: *parent21, Parent2: *parent22, Name: "franco"} + err = ts.db.Create(child2).Error + ts.Nil(err) + + children, err := orm.NewQuery[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Ascending(conditions.ParentParent.Name, 0).Find() + ts.Nil(err) + + ts.Len(children, 2) + assert.DeepEqual(ts.T(), child1, children[0]) + assert.DeepEqual(ts.T(), child2, children[1]) +} + +// ------------------------- Limit -------------------------------- + +func (ts *QueryIntTestSuite) TestLimitLimitsTheAmountOfModelsReturned() { + product1 := ts.createProduct("", 1, 0, false, nil) + product2 := ts.createProduct("", 1, 0, false, nil) + products, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Limit(1).Find() + ts.Nil(err) + + ts.Len(products, 1) + ts.True(cmp.Equal(products[0], product1) || cmp.Equal(products[0], product2)) +} + +func (ts *QueryIntTestSuite) TestLimitCanBeCanceled() { + product1 := ts.createProduct("", 1, 0, false, nil) + product2 := ts.createProduct("", 1, 0, false, nil) + products, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Limit(1).Limit(-1).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{product1, product2}, products) +} + +// ------------------------- Offset -------------------------------- + +func (ts *QueryIntTestSuite) TestOffsetSkipsTheModelsReturned() { + ts.createProduct("", 1, 1, false, nil) + product2 := ts.createProduct("", 1, 2, false, nil) + + switch getDBDialector() { + case query.Postgres, query.SQLServer, query.SQLite: + products, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Ascending(conditions.Product.Float).Offset(1).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{product2}, products) + case query.MySQL: + products, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Ascending(conditions.Product.Float).Offset(1).Limit(10).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{product2}, products) + } +} + +func (ts *QueryIntTestSuite) TestOffsetReturnsEmptyIfMoreOffsetThanResults() { + ts.createProduct("", 1, 0, false, nil) + ts.createProduct("", 1, 0, false, nil) + + switch getDBDialector() { + case query.Postgres, query.SQLServer, query.SQLite: + products, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Offset(2).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{}, products) + case query.MySQL: + products, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Offset(2).Limit(10).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{}, products) + } +} + +func (ts *QueryIntTestSuite) TestOffsetAndLimitWorkTogether() { + ts.createProduct("", 1, 1, false, nil) + product2 := ts.createProduct("", 1, 2, false, nil) + ts.createProduct("", 1, 3, false, nil) + products, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Ascending(conditions.Product.Float).Offset(1).Limit(1).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{product2}, products) +} diff --git a/testintegration/where_conditions_test.go b/testintegration/where_conditions_test.go new file mode 100644 index 00000000..446a769c --- /dev/null +++ b/testintegration/where_conditions_test.go @@ -0,0 +1,578 @@ +package testintegration + +import ( + "log" + + "gorm.io/gorm" + + "github.com/ditrit/badaas/orm" + "github.com/ditrit/badaas/orm/errors" + "github.com/ditrit/badaas/orm/mysql" + "github.com/ditrit/badaas/orm/query" + "github.com/ditrit/badaas/orm/unsafe" + "github.com/ditrit/badaas/testintegration/conditions" + "github.com/ditrit/badaas/testintegration/models" +) + +type WhereConditionsIntTestSuite struct { + ORMIntTestSuite +} + +func NewWhereConditionsIntTestSuite( + db *gorm.DB, +) *WhereConditionsIntTestSuite { + return &WhereConditionsIntTestSuite{ + ORMIntTestSuite: ORMIntTestSuite{ + db: db, + }, + } +} + +func (ts *WhereConditionsIntTestSuite) TestQueryReturnsEmptyIfNotEntitiesCreated() { + entities, err := orm.NewQuery[models.Product](ts.db).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestQueryReturnsTheOnlyOneIfOneEntityCreated() { + match := ts.createProduct("", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product](ts.db).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestQueryReturnsTheListWhenMultipleCreated() { + match1 := ts.createProduct("", 0, 0, false, nil) + match2 := ts.createProduct("", 0, 0, false, nil) + match3 := ts.createProduct("", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product](ts.db).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2, match3}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionsReturnsEmptyIfNotEntitiesCreated() { + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Eq("not_created"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionsReturnsEmptyIfNothingMatch() { + ts.createProduct("something_else", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Eq("not_match"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionsReturnsOneIfOnlyOneMatch() { + match := ts.createProduct("match", 0, 0, false, nil) + ts.createProduct("not_match", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Eq("match"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionsReturnsMultipleIfMultipleMatch() { + match1 := ts.createProduct("match", 0, 0, false, nil) + match2 := ts.createProduct("match", 0, 0, false, nil) + ts.createProduct("not_match", 0, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Eq("match"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfIntType() { + match := ts.createProduct("match", 1, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().Eq(1), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfFloatType() { + match := ts.createProduct("match", 0, 1.1, false, nil) + ts.createProduct("not_match", 0, 2.2, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.FloatIs().Eq(1.1), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfBoolType() { + match := ts.createProduct("match", 0, 0.0, true, nil) + ts.createProduct("not_match", 0, 0.0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.BoolIs().Eq(true), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestMultipleConditionsOfDifferentTypesWorks() { + match1 := ts.createProduct("match", 1, 0.0, true, nil) + match2 := ts.createProduct("match", 1, 0.0, true, nil) + + ts.createProduct("not_match", 1, 0.0, true, nil) + ts.createProduct("match", 2, 0.0, true, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Eq("match"), + conditions.Product.IntIs().Eq(1), + conditions.Product.BoolIs().Eq(true), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfID() { + match := ts.createProduct("", 0, 0.0, false, nil) + ts.createProduct("", 0, 0.0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IdIs().Eq(match.ID), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfCreatedAt() { + match := ts.createProduct("", 0, 0.0, false, nil) + ts.createProduct("", 0, 0.0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.CreatedAtIs().Eq(match.CreatedAt), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestDeletedAtConditionIsAddedAutomatically() { + match := ts.createProduct("", 0, 0.0, false, nil) + deleted := ts.createProduct("", 0, 0.0, false, nil) + + ts.Nil(ts.db.Delete(deleted).Error) + + entities, err := orm.NewQuery[models.Product](ts.db).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfDeletedAt() { + match := ts.createProduct("", 0, 0.0, false, nil) + ts.createProduct("", 0, 0.0, false, nil) + + ts.Nil(ts.db.Delete(match).Error) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.DeletedAtIs().Eq(match.DeletedAt.Time), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfEmbedded() { + match := ts.createProduct("", 0, 0.0, false, nil) + ts.createProduct("", 0, 0.0, false, nil) + + match.EmbeddedInt = 1 + + err := ts.db.Save(match).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.ToBeEmbeddedEmbeddedIntIs().Eq(1), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfGormEmbedded() { + match := ts.createProduct("", 0, 0.0, false, nil) + ts.createProduct("", 0, 0.0, false, nil) + + match.GormEmbedded.Int = 1 + + err := ts.db.Save(match).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.GormEmbeddedIntIs().Eq(1), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfPointerTypeWithValue() { + intMatch := 1 + match := ts.createProduct("match", 1, 0, false, &intMatch) + intNotMatch := 2 + ts.createProduct("not_match", 2, 0, false, &intNotMatch) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntPointerIs().Eq(1), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfByteArrayWithContent() { + match := ts.createProduct("match", 1, 0, false, nil) + notMatch1 := ts.createProduct("not_match", 2, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + match.ByteArray = []byte{1, 2} + notMatch1.ByteArray = []byte{2, 3} + + err := ts.db.Save(match).Error + ts.Nil(err) + + err = ts.db.Save(notMatch1).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.ByteArrayIs().Eq([]byte{1, 2}), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfByteArrayEmpty() { + match := ts.createProduct("match", 1, 0, false, nil) + notMatch1 := ts.createProduct("not_match", 2, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + match.ByteArray = []byte{} + notMatch1.ByteArray = []byte{2, 3} + + err := ts.db.Save(match).Error + ts.Nil(err) + + err = ts.db.Save(notMatch1).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.ByteArrayIs().Eq([]byte{}), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfCustomType() { + match := ts.createProduct("match", 1, 0, false, nil) + notMatch1 := ts.createProduct("not_match", 2, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + match.MultiString = models.MultiString{"salut", "hola"} + notMatch1.MultiString = models.MultiString{"salut", "hola", "hello"} + + err := ts.db.Save(match).Error + ts.Nil(err) + + err = ts.db.Save(notMatch1).Error + ts.Nil(err) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.MultiStringIs().Eq(models.MultiString{"salut", "hola"}), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfRelationType() { + product1 := ts.createProduct("", 0, 0.0, false, nil) + product2 := ts.createProduct("", 0, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + seller2 := ts.createSeller("agustin", nil) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.ProductIdIs().Eq(product1.ID), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfRelationTypeOptionalWithValue() { + product1 := ts.createProduct("", 0, 0.0, false, nil) + product2 := ts.createProduct("", 0, 0.0, false, nil) + + seller1 := ts.createSeller("franco", nil) + seller2 := ts.createSeller("agustin", nil) + + match := ts.createSale(0, product1, seller1) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.SellerIdIs().Eq(seller1.ID), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionOfRelationTypeOptionalByNil() { + product1 := ts.createProduct("", 0, 0.0, false, nil) + product2 := ts.createProduct("", 0, 0.0, false, nil) + + seller2 := ts.createSeller("agustin", nil) + + match := ts.createSale(0, product1, nil) + ts.createSale(0, product2, seller2) + + entities, err := orm.NewQuery[models.Sale]( + ts.db, + conditions.Sale.SellerIdIs().Null(), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Sale{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestConditionsOnUIntModel() { + match := ts.createBrand("match") + ts.createBrand("not_match") + + entities, err := orm.NewQuery[models.Brand]( + ts.db, + conditions.Brand.NameIs().Eq("match"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Brand{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestMultipleConditionsAreConnectedByAnd() { + match := ts.createProduct("match", 3, 0, false, nil) + ts.createProduct("not_match", 5, 0, false, nil) + ts.createProduct("not_match", 1, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.IntIs().GtOrEq(3), + conditions.Product.IntIs().LtOrEq(4), + conditions.Product.StringIs().Eq("match"), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestNot() { + match1 := ts.createProduct("match", 1, 0, false, nil) + match2 := ts.createProduct("match", 3, 0, false, nil) + + ts.createProduct("not_match", 2, 0, false, nil) + ts.createProduct("not_match", 2, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + orm.Not(conditions.Product.IntIs().Eq(2)), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestNotWithMultipleConditionsAreConnectedByAnd() { + match1 := ts.createProduct("match", 1, 0, false, nil) + match2 := ts.createProduct("match", 5, 0, false, nil) + + ts.createProduct("not_match", 2, 0, false, nil) + ts.createProduct("not_match", 3, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + orm.Not( + conditions.Product.IntIs().Gt(1), + conditions.Product.IntIs().Lt(4), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestOr() { + match1 := ts.createProduct("match", 2, 0, false, nil) + match2 := ts.createProduct("match", 3, 0, false, nil) + match3 := ts.createProduct("match_3", 3, 0, false, nil) + + ts.createProduct("not_match", 1, 0, false, nil) + ts.createProduct("not_match", 4, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + orm.Or( + conditions.Product.IntIs().Eq(2), + conditions.Product.IntIs().Eq(3), + conditions.Product.StringIs().Eq("match_3"), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2, match3}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestNotOr() { + match1 := ts.createProduct("match", 1, 0, false, nil) + match2 := ts.createProduct("match", 5, 0, false, nil) + match3 := ts.createProduct("match", 4, 0, false, nil) + + ts.createProduct("not_match", 2, 0, false, nil) + ts.createProduct("not_match_string", 3, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + orm.Not[models.Product]( + orm.Or( + conditions.Product.IntIs().Eq(2), + conditions.Product.StringIs().Eq("not_match_string"), + ), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2, match3}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestXor() { + switch getDBDialector() { + case query.Postgres, query.SQLite, query.SQLServer: + log.Println("Xor not compatible") + case query.MySQL: + match1 := ts.createProduct("", 1, 0, false, nil) + match2 := ts.createProduct("", 7, 0, false, nil) + + ts.createProduct("", 5, 0, false, nil) + ts.createProduct("", 4, 0, false, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + mysql.Xor( + conditions.Product.IntIs().Lt(6), + conditions.Product.IntIs().Gt(3), + ), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) + } +} + +func (ts *WhereConditionsIntTestSuite) TestMultipleConditionsDifferentOperators() { + match1 := ts.createProduct("match", 1, 0.0, true, nil) + match2 := ts.createProduct("match", 1, 0.0, true, nil) + + ts.createProduct("not_match", 1, 0.0, true, nil) + ts.createProduct("match", 2, 0.0, true, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + conditions.Product.StringIs().Eq("match"), + conditions.Product.IntIs().Lt(2), + conditions.Product.BoolIs().NotEq(false), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestUnsafeCondition() { + match1 := ts.createProduct("match", 1, 0.0, true, nil) + match2 := ts.createProduct("match", 1, 0.0, true, nil) + + ts.createProduct("not_match", 2, 0.0, true, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + unsafe.NewCondition[models.Product]("%s.int = ?", 1), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestEmptyConnectionConditionMakesNothing() { + match1 := ts.createProduct("match", 1, 0.0, true, nil) + match2 := ts.createProduct("match", 1, 0.0, true, nil) + + entities, err := orm.NewQuery[models.Product]( + ts.db, + orm.And[models.Product](), + ).Find() + ts.Nil(err) + + EqualList(&ts.Suite, []*models.Product{match1, match2}, entities) +} + +func (ts *WhereConditionsIntTestSuite) TestEmptyContainerConditionReturnsError() { + _, err := orm.NewQuery[models.Product]( + ts.db, + orm.Not[models.Product](), + ).Find() + ts.ErrorIs(err, errors.ErrEmptyConditions) +} diff --git a/utils/slice.go b/utils/slice.go new file mode 100644 index 00000000..2b370951 --- /dev/null +++ b/utils/slice.go @@ -0,0 +1,15 @@ +package utils + +import ( + "github.com/elliotchance/pie/v2" +) + +func FindFirst[T any](ss []T, fn func(value T) bool) *T { + index := pie.FindFirstUsing(ss, fn) + + if index == -1 { + return nil + } + + return &ss[index] +} diff --git a/utils/slice_test.go b/utils/slice_test.go new file mode 100644 index 00000000..df05c090 --- /dev/null +++ b/utils/slice_test.go @@ -0,0 +1,59 @@ +package utils_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ditrit/badaas/utils" +) + +var ( + testResult3 = 33.04 + testResult4 = 0.11 +) + +var findFirstTests = []struct { + ss []float64 + expression func(value float64) bool + expected *float64 +}{ + { + nil, + func(value float64) bool { return value == 1.5 }, + nil, + }, + { + []float64{}, + func(value float64) bool { return value == 0.1 }, + nil, + }, + { + []float64{0.0, 1.5, 3.2}, + func(value float64) bool { return value == 9.99 }, + nil, + }, + { + []float64{5.4, 6.98, 4.987, 33.04}, + func(value float64) bool { return value == 33.04 }, + &testResult3, + }, + { + []float64{9.0, 0.11, 150.44, 33.04}, + func(value float64) bool { return value == 0.11 }, + &testResult4, + }, +} + +func TestFindFirst(t *testing.T) { + for _, test := range findFirstTests { + t.Run("", func(t *testing.T) { + result := utils.FindFirst(test.ss, test.expression) + if result == nil { + assert.Nil(t, test.expected) + } else { + assert.Equal(t, *test.expected, *result) + } + }) + } +} diff --git a/configuration/time.go b/utils/time.go similarity index 60% rename from configuration/time.go rename to utils/time.go index f71ef280..b075cbb1 100644 --- a/configuration/time.go +++ b/utils/time.go @@ -1,8 +1,8 @@ -package configuration +package utils import "time" // Convert int (seconds) to [time.Duration] -func intToSecond(numberOfSeconds int) time.Duration { +func IntToSecond(numberOfSeconds int) time.Duration { return time.Duration(numberOfSeconds) * time.Second } diff --git a/configuration/time_test.go b/utils/time_test.go similarity index 62% rename from configuration/time_test.go rename to utils/time_test.go index 56134aab..6261ca46 100644 --- a/configuration/time_test.go +++ b/utils/time_test.go @@ -1,4 +1,4 @@ -package configuration +package utils import ( "testing" @@ -10,20 +10,20 @@ import ( func TestIntToSecond(t *testing.T) { assert.Equal( t, - intToSecond(20), - time.Duration(20*time.Second), + IntToSecond(20), + 20*time.Second, "the duration should be equals", ) assert.Equal( t, - intToSecond(-5), - time.Duration(-5*time.Second), + IntToSecond(-5), + -5*time.Second, "the duration should be equals", ) assert.Equal( t, - intToSecond(3600), - time.Duration(time.Hour), + IntToSecond(3600), + time.Hour, "the duration should be equals", ) } diff --git a/validators/email.go b/utils/validators/email.go similarity index 92% rename from validators/email.go rename to utils/validators/email.go index 99d9d1e1..1a10ab72 100644 --- a/validators/email.go +++ b/utils/validators/email.go @@ -1,4 +1,4 @@ -package validator +package validators import "net/mail" @@ -8,5 +8,6 @@ func ValidEmail(email string) (string, error) { if err != nil { return "", err } + return addr.Address, nil } diff --git a/validators/email_test.go b/utils/validators/email_test.go similarity index 53% rename from validators/email_test.go rename to utils/validators/email_test.go index c41f0853..600741ed 100644 --- a/validators/email_test.go +++ b/utils/validators/email_test.go @@ -1,30 +1,31 @@ -package validator_test +package validators_test import ( "testing" - validator "github.com/ditrit/badaas/validators" "github.com/stretchr/testify/assert" + + "github.com/ditrit/badaas/utils/validators" ) func TestValidEmail(t *testing.T) { - mail, err := validator.ValidEmail("bob.bobemail.com") + mail, err := validators.ValidEmail("bob.bobemail.com") assert.Error(t, err) assert.Equal(t, "", mail) - mail, err = validator.ValidEmail("bob.bob@") + mail, err = validators.ValidEmail("bob.bob@") assert.Error(t, err) assert.Equal(t, "", mail) - mail, err = validator.ValidEmail("bob.bob@email.com") + mail, err = validators.ValidEmail("bob.bob@email.com") assert.NoError(t, err) assert.Equal(t, "bob.bob@email.com", mail) - mail, err = validator.ValidEmail("Gopher ") + mail, err = validators.ValidEmail("Gopher ") assert.NoError(t, err) assert.Equal(t, "from@example.com", mail) - mail, err = validator.ValidEmail("bob.bob%@email.com") + mail, err = validators.ValidEmail("bob.bob%@email.com") assert.NoError(t, err) assert.Equal(t, "bob.bob%@email.com", mail) }