feat(queries): use org projection (#2342)

* job queue

* wg improvements

* start handler

* statement

* statements

* imporve handler

* improve statement

* statement in seperate file

* move handlers

* move query/old to query

* handler

* read models

* bulk works

* cleanup

* contrib

* rename readmodel to projection

* rename read_models schema to projections

* rename read_models schema to projections

* search query as func,
bulk iterates as long as new events

* add event sequence less query

* update checks for events between current sequence and sequence of first statement if it has previous sequence 0

* cleanup crdb projection

* refactor projection handler

* start with testing

* tests for handler

* remove todo

* refactor statement: remove table name,
add tests

* improve projection handler shutdown,
no savepoint if noop stmt,
tests for stmt handler

* tests

* start failed events

* seperate branch for contrib

* move statement constructors to crdb pkg

* correct import

* Subscribe for eventtypes (#1800)

* fix: is default (#1737)

* fix: use email as username on global org (#1738)

* fix: use email as username on global org

* Update user_human.go

* Update register_handler.go

* chore(deps): update docusaurus (#1739)

* chore: remove PAT and use GH Token (#1716)

* chore: remove PAT and use GH Token

* fix env

* fix env

* fix env

* md lint

* trigger ci

* change user

* fix GH bug

* replace login part

* chore: add GH Token to sem rel (#1746)

* chore: add GH Token to sem rel

* try branch

* add GH Token

* remove test branch again

* docs: changes acme to acme-caos (#1744)

* changes acme to acme-caos

* Apply suggestions from code review

Co-authored-by: Florian Forster <florian@caos.ch>

Co-authored-by: Maximilian Panne <maximilian.panne@gmail.com>
Co-authored-by: Florian Forster <florian@caos.ch>

* feat: add additional origins on applications (#1691)

* feat: add additional origins on applications

* app additional redirects

* chore(deps-dev): bump @angular/cli from 11.2.8 to 11.2.11 in /console (#1706)

* fix: show org with regex (#1688)

* fix: flag mapping (#1699)

* chore(deps-dev): bump @angular/cli from 11.2.8 to 11.2.11 in /console

Bumps [@angular/cli](https://github.com/angular/angular-cli) from 11.2.8 to 11.2.11.
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Commits](https://github.com/angular/angular-cli/compare/v11.2.8...v11.2.11)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump stylelint from 13.10.0 to 13.13.1 in /console (#1703)

* fix: show org with regex (#1688)

* fix: flag mapping (#1699)

* chore(deps-dev): bump stylelint from 13.10.0 to 13.13.1 in /console

Bumps [stylelint](https://github.com/stylelint/stylelint) from 13.10.0 to 13.13.1.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/13.10.0...13.13.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @types/node from 14.14.37 to 15.0.1 in /console (#1702)

* fix: show org with regex (#1688)

* fix: flag mapping (#1699)

* chore(deps-dev): bump @types/node from 14.14.37 to 15.0.1 in /console

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.37 to 15.0.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump ts-protoc-gen from 0.14.0 to 0.15.0 in /console (#1701)

* fix: show org with regex (#1688)

* fix: flag mapping (#1699)

* chore(deps): bump ts-protoc-gen from 0.14.0 to 0.15.0 in /console

Bumps [ts-protoc-gen](https://github.com/improbable-eng/ts-protoc-gen) from 0.14.0 to 0.15.0.
- [Release notes](https://github.com/improbable-eng/ts-protoc-gen/releases)
- [Changelog](https://github.com/improbable-eng/ts-protoc-gen/blob/master/CHANGELOG.md)
- [Commits](https://github.com/improbable-eng/ts-protoc-gen/compare/0.14.0...0.15.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @types/jasmine from 3.6.9 to 3.6.10 in /console (#1682)

Bumps [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine) from 3.6.9 to 3.6.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jasmine)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @types/google-protobuf in /console (#1681)

Bumps [@types/google-protobuf](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/google-protobuf) from 3.7.4 to 3.15.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/google-protobuf)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump grpc from 1.24.5 to 1.24.7 in /console (#1666)

Bumps [grpc](https://github.com/grpc/grpc-node) from 1.24.5 to 1.24.7.
- [Release notes](https://github.com/grpc/grpc-node/releases)
- [Commits](https://github.com/grpc/grpc-node/compare/grpc@1.24.5...grpc@1.24.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* lock

* chore(deps-dev): bump @angular/language-service from 11.2.9 to 11.2.12 in /console (#1704)

* fix: show org with regex (#1688)

* fix: flag mapping (#1699)

* chore(deps-dev): bump @angular/language-service in /console

Bumps [@angular/language-service](https://github.com/angular/angular/tree/HEAD/packages/language-service) from 11.2.9 to 11.2.12.
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/11.2.12/packages/language-service)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* package lock

* downgrade grpc

* downgrade protobuf types

* revert npm packs 🥸

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Silvan <silvan.reusser@gmail.com>

* docs: update run and start section texts (#1745)

* update run and start section texts

* adds showcase

Co-authored-by: Maximilian Panne <maximilian.panne@gmail.com>

* fix: additional origin list (#1753)

* fix: handle api configs in authz handler (#1755)

* fix(console): add model for api keys, fix toast, binding (#1757)

* fix: add model for api keys, fix toast, binding

* show api clientid

* fix: missing patchvalue (#1758)

* feat: refresh token (#1728)

* begin refresh tokens

* refresh tokens

* list and revoke refresh tokens

* handle remove

* tests for refresh tokens

* uniqueness and default expiration

* rename oidc token methods

* cleanup

* migration version

* Update internal/static/i18n/en.yaml

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* fixes

* feat: update oidc pkg for refresh tokens

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* fix: correct json name of clientId in key.json (#1760)

* fix: migration version (#1767)

* start subscription

* eventtypes

* fix(login): links (#1778)

* fix(login): href for help

* fix(login): correct link to tos

* fix: access tokens for service users and refresh token infos (#1779)

* fix: access token for service user

* handle info from refresh request

* uniqueness

* postpone access token uniqueness change

* chore(coc): recommend code of conduct (#1782)

* subscribe for events

* feat(console): refresh toggle out of granttype context (#1785)

* refresh toggle

* disable if not code flow, lint

* lint

* fix: change oidc config order

* accept refresh option within flow

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* fix: refresh token activation (#1795)

* fix: oidc grant type check

* docs: add offline_access scope

* docs: update refresh token status in supported grant types

* fix: update oidc pkg

* fix: check refresh token grant type (#1796)

* configuration structs

* org admins

* failed events

* fixes

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Florian Forster <florian@caos.ch>
Co-authored-by: mffap <mpa@caos.ch>
Co-authored-by: Maximilian Panne <maximilian.panne@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* remove comment

* aggregate reducer

* remove eventtypes

* add protoc-get-validate to mod

* fix transaltion

* upsert

* add gender on org admins,
allow to retry failed stmts after configurable time

* remove if

* sub queries

* fix: tests

* add builder to tests

* new search query

* rename searchquerybuilder to builder

* remove comment from code

* test with multiple queries

* add filters test

* current sequences

* make org and org_admins work again

* add aggregate type to current sequence

* fix(contibute): listing

* add validate module

* fix: search queries

* feat(eventstore): previous aggregate root sequence (#1810)

* feat(eventstore): previous aggregate root sequence

* fix tests

* fix: eventstore v1 test

* add col to all mocked rows

* next try

* fix mig

* rename aggregate root to aggregate type

* update comment

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* small refactorings

* allow update multiple current sequences

* unique log id

* fix migrations

* rename org admin to org owner

* improve error handling and logging

* fix(migration): optimize prev agg root seq

* fix: projection handler test

* fix: sub queries

* small fixes

* additional event types

* correct org owner projection

* fix primary key

* feat(eventstore): jobs for projections (#2026)

* fix: template names in login (#1974)

* fix: template names in login

* fix: error.html

* fix: check for features on mgmt only (#1976)

* fix: add sentry in ui, http and projection handlers (#1977)

* fix: add sentry in ui, http and projection handlers

* fix test

* fix(eventstore): sub queries (#1805)

* sub queries

* fix: tests

* add builder to tests

* new search query

* rename searchquerybuilder to builder

* remove comment from code

* test with multiple queries

* add filters test

* fix(contibute): listing

* add validate module

* fix: search queries

* remove unused event type in query

* ignore query if error in marshal

* go mod tidy

* update privacy policy query

* update queries

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* feat: Extend oidc idp with oauth endpoints (#1980)

* feat: add oauth attributes to oidc idp configuration

* feat: return idpconfig id on create idp

* feat: tests

* feat: descriptions

* feat: docs

* feat: tests

* docs: update to beta 3 (#1984)

* fix: role assertion (#1986)

* fix: enum to display access token role assertion

* improve assertion descriptions

* fix nil pointer

* docs: eventstore (#1982)

* docs: eventstore

* Apply suggestions from code review

Co-authored-by: Florian Forster <florian@caos.ch>

Co-authored-by: Florian Forster <florian@caos.ch>

* fix(sentry): trigger sentry release (#1989)

* feat(send sentry release): send sentry release

* fix(moved step and added releasetag): moved step and added releasetag

* fix: set version for sentry release (#1990)

* feat(send sentry release): send sentry release

* fix(moved step and added releasetag): moved step and added releasetag

* fix(corrected var name): corrected var name

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* fix: log error reason on terminate session (#1973)

* fix: return default language file, if requested lang does not exist for default login texts (#1988)

* fix: return default language file, if requested lang doesnt exists

* feat: read default translation file

* feat: docs

* fix: race condition in auth request unmarshalling (#1993)

* feat: handle ui_locales in login (#1994)

* fix: handle ui_locales in login

* move supportedlanguage func into i18n package

* update oidc pkg

* fix: handle closed channels on unsubscribe (#1995)

* fix: give restore more time (#1997)

* fix: translation file read (#2009)

* feat: translation file read

* feat: readme

* fix: enable idp add button for iam users (#2010)

* fix: filter event_data (#2011)

* feat: Custom message files (#1992)

* feat: add get custom message text to admin api

* feat: read custom message texts from files

* feat: get languages in apis

* feat: get languages in apis

* feat: get languages in apis

* feat: pr feedback

* feat: docs

* feat: merge main

* fix: sms notification (#2013)

* fix: phone verifications

* feat: fix password reset as sms

* fix: phone verification

* fix: grpc status in sentry and validation interceptors (#2012)

* fix: remove oauth endpoints from oidc config proto (#2014)

* try with view

* fix(console): disable sw (#2021)

* fix: disable sw

* angular.json disable sw

* project projections

* fix typos

* customize projections

* customizable projections,
add change date to projects

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Florian Forster <florian@caos.ch>
Co-authored-by: mffap <mpa@caos.ch>
Co-authored-by: Christian Jakob <47860090+thesephirot@users.noreply.github.com>
Co-authored-by: Elio Bischof <eliobischof@gmail.com>

* env file

* typo

* correct users

* correct migration

* fix: merge fail

* fix test

* fix(tests): unordered matcher

* improve currentSequenceMatcher

* correct certs

* correct certs

* add zitadel database on database list

* refctor switch in match

* enable all handlers

* Delete io.env

* cleanup

* add handlers

* rename view to projection

* rename view to projection

* fix type typo

* remove unnecessary logs

* refactor stmts

* simplify interval calculation

* fix tests

* fix unlock test

* fix migration

* migs

* fix(operator): update cockroach and flyway versions (#2138)

* chore(deps): bump k8s.io/apiextensions-apiserver from 0.19.2 to 0.21.3

Bumps [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) from 0.19.2 to 0.21.3.
- [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases)
- [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.19.2...v0.21.3)

---
updated-dependencies:
- dependency-name: k8s.io/apiextensions-apiserver
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump google.golang.org/api from 0.34.0 to 0.52.0

Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.34.0 to 0.52.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.34.0...v0.52.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* start update dependencies

* update mods and otlp

* fix(build): update to go 1.16

* old version for k8s mods

* update k8s versions

* update orbos

* fix(operator): update cockroach and flyway version

* Update images.go

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Stefan Benz <stefan@caos.ch>

* fix import

* fix typo

* fix(migration): add org projection

* fix(projection): correct table for org events in org owners

* better insert stmt

* fix typo

* fix typo

* set max connection lifetime

* set max conns and conn lifetime in eventstore v1

* configure sql connection settings

* add mig for agg type index

* fix replace tab in yaml

* handler interfaces

* subscription

* first try

* handler

* move sql client initialization

* first part implemented

* removed all occurencies of org by id and search orgs

* fix merge issues

* cleanup code

* fix: queries implements orgviewprovider

* cleanup

* refactor text comparison

* remove unused file

* remove unused code

* log

* remove unused code

* remove unused field

* remove unused file

* refactor

* tests for search query

* remove try

* simplify state change mappers

* projection tests

* query functions

* move reusable objects to separate files

* rename domain column to primar_domain

* fix tests

* add current sequence

* remove log prints

* fix tests

* fix: verifier

* fix test

* rename domain col migrations

* simplify search response

* add custom column constructors

* fix: org projection table const

* fix: full column name

* feat: text query extension

* fix: tests for query

* number query

* add deprection message

* column in a single place (#2416)

* column in a single place

* use projection for columns

* query column with aliases

* rename methods

* remove unused code

* column for current sequences

* global counter column

* fix is org unique

* fix: merge main and change actions / flow projections to new query side (#2434)

* feat: actions (#2377)

* feat(actions): begin api

* feat(actions): begin api

* api and projections

* fix: handle multiple statements for a single event in projections

* export func type

* fix test

* update to new reduce interface

* flows in login

* feat: jwt idp

* feat: command side

* feat: add tests

* actions and flows

* fill idp views with jwt idps and return apis

* add jwtEndpoint to jwt idp

* begin jwt request handling

* add feature

* merge

* merge

* handle jwt idp

* cleanup

* bug fixes

* autoregister

* get token from specific header name

* fix: proto

* fixes

* i18n

* begin tests

* fix and log http proxy

* remove docker cache

* fixes

* usergrants in actions api

* tests adn cleanup

* cleanup

* fix add user grant

* set login context

* i18n

Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>

* change actions / flow projections to new query side

* fixes

* enable org projection

Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>

* fixes

* cleanup

* add tests

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Florian Forster <florian@caos.ch>
Co-authored-by: mffap <mpa@caos.ch>
Co-authored-by: Maximilian Panne <maximilian.panne@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Christian Jakob <47860090+thesephirot@users.noreply.github.com>
Co-authored-by: Elio Bischof <eliobischof@gmail.com>
Co-authored-by: Stefan Benz <stefan@caos.ch>
Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>
This commit is contained in:
Silvan
2021-09-29 13:20:57 +02:00
committed by GitHub
parent 5e110f0a48
commit 39c35c9455
80 changed files with 2960 additions and 1549 deletions

View File

@@ -2,76 +2,80 @@ package query
import (
"context"
"database/sql"
errs "errors"
"time"
"github.com/Masterminds/squirrel"
sq "github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/query/projection"
)
var actionsQuery = squirrel.StatementBuilder.Select("creation_date", "change_date", "resource_owner", "sequence", "id", "action_state", "name", "script", "timeout", "allowed_to_fail").
From("zitadel.projections.actions").PlaceholderFormat(squirrel.Dollar)
func (q *Queries) GetAction(ctx context.Context, id string, orgID string) (*Action, error) {
idQuery, _ := newActionIDSearchQuery(id)
actions, err := q.SearchActions(ctx, &ActionSearchQueries{Queries: []SearchQuery{idQuery}})
if err != nil {
return nil, err
var (
actionTable = table{
name: projection.ActionTable,
}
if len(actions) != 1 {
return nil, errors.ThrowNotFound(nil, "QUERY-dft2g", "Errors.Action.NotFound")
ActionColumnID = Column{
name: projection.ActionIDCol,
table: actionTable,
}
return actions[0], err
}
func (q *Queries) SearchActions(ctx context.Context, query *ActionSearchQueries) ([]*Action, error) {
stmt, args, err := query.ToQuery(actionsQuery).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-wQ3by", "Errors.orgs.invalid.request")
ActionColumnCreationDate = Column{
name: projection.ActionCreationDateCol,
table: actionTable,
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-M6mYN", "Errors.orgs.internal")
ActionColumnChangeDate = Column{
name: projection.ActionChangeDateCol,
table: actionTable,
}
actions := []*Action{}
for rows.Next() {
org := new(Action)
rows.Scan(
&org.CreationDate,
&org.ChangeDate,
&org.ResourceOwner,
&org.Sequence,
&org.ID,
&org.State,
&org.Name,
&org.Script,
&org.Timeout,
&org.AllowedToFail,
)
actions = append(actions, org)
ActionColumnResourceOwner = Column{
name: projection.ActionResourceOwnerCol,
table: actionTable,
}
if err := rows.Err(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-pA0Wj", "Errors.actions.internal")
ActionColumnSequence = Column{
name: projection.ActionSequenceCol,
table: actionTable,
}
ActionColumnState = Column{
name: projection.ActionStateCol,
table: actionTable,
}
ActionColumnName = Column{
name: projection.ActionNameCol,
table: actionTable,
}
ActionColumnScript = Column{
name: projection.ActionScriptCol,
table: actionTable,
}
ActionColumnTimeout = Column{
name: projection.ActionTimeoutCol,
table: actionTable,
}
ActionColumnAllowedToFail = Column{
name: projection.ActionAllowedToFailCol,
table: actionTable,
}
)
return actions, nil
type Actions struct {
SearchResponse
Actions []*Action
}
type Action struct {
ID string `col:"id"`
CreationDate time.Time `col:"creation_date"`
ChangeDate time.Time `col:"change_date"`
ResourceOwner string `col:"resource_owner"`
State domain.ActionState `col:"action_state"`
Sequence uint64 `col:"sequence"`
ID string
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
State domain.ActionState
Sequence uint64
Name string `col:"name"`
Script string `col:"script"`
Timeout time.Duration `col:"-"`
AllowedToFail bool `col:"-"`
Name string
Script string
Timeout time.Duration
AllowedToFail bool
}
type ActionSearchQueries struct {
@@ -79,26 +83,146 @@ type ActionSearchQueries struct {
Queries []SearchQuery
}
func (q *ActionSearchQueries) ToQuery(query squirrel.SelectBuilder) squirrel.SelectBuilder {
query = q.SearchRequest.ToQuery(query)
func (q *ActionSearchQueries) ToQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = q.SearchRequest.toQuery(query)
for _, q := range q.Queries {
query = q.ToQuery(query)
}
return query
}
func (q *Queries) SearchActions(ctx context.Context, queries *ActionSearchQueries) (actions *Actions, err error) {
query, scan := prepareActionsQuery()
stmt, args, err := queries.toQuery(query).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-SDgwg", "Errors.Query.InvalidRequest")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-SDfr52", "Errors.Internal")
}
actions, err = scan(rows)
if err != nil {
return nil, err
}
actions.LatestSequence, err = q.latestSequence(ctx, actionTable)
return actions, err
}
func (q *Queries) GetActionByID(ctx context.Context, id string, orgID string) (*Action, error) {
stmt, scan := prepareActionQuery()
query, args, err := stmt.Where(
sq.Eq{
ActionColumnID.identifier(): id,
},
sq.Eq{
ActionColumnResourceOwner.identifier(): orgID,
}).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-Dgff3", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, query, args...)
return scan(row)
}
func NewActionResourceOwnerQuery(id string) (SearchQuery, error) {
return NewTextQuery("resource_owner", id, TextEquals)
return NewTextQuery(ActionColumnResourceOwner, id, TextEquals)
}
func NewActionNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
return NewTextQuery("name", value, method)
return NewTextQuery(ActionColumnName, value, method)
}
func NewActionStateSearchQuery(value domain.ActionState) (SearchQuery, error) {
return NewIntQuery("state", int(value), IntEquals)
return NewNumberQuery(ActionColumnState, int(value), NumberEquals)
}
func newActionIDSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery("id", id, TextEquals)
func prepareActionsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Actions, error)) {
return sq.Select(
ActionColumnID.identifier(),
ActionColumnCreationDate.identifier(),
ActionColumnChangeDate.identifier(),
ActionColumnResourceOwner.identifier(),
ActionColumnSequence.identifier(),
ActionColumnState.identifier(),
ActionColumnName.identifier(),
ActionColumnScript.identifier(),
ActionColumnTimeout.identifier(),
ActionColumnAllowedToFail.identifier(),
countColumn.identifier(),
).From(actionTable.identifier()).PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*Actions, error) {
actions := make([]*Action, 0)
var count uint64
for rows.Next() {
action := new(Action)
err := rows.Scan(
&action.ID,
&action.CreationDate,
&action.ChangeDate,
&action.ResourceOwner,
&action.Sequence,
&action.State,
&action.Name,
&action.Script,
&action.Timeout,
&action.AllowedToFail,
&count,
)
if err != nil {
return nil, err
}
actions = append(actions, action)
}
if err := rows.Close(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-EGdff", "Errors.Query.CloseRows")
}
return &Actions{
Actions: actions,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
}
func prepareActionQuery() (sq.SelectBuilder, func(row *sql.Row) (*Action, error)) {
return sq.Select(
ActionColumnID.identifier(),
ActionColumnCreationDate.identifier(),
ActionColumnChangeDate.identifier(),
ActionColumnResourceOwner.identifier(),
ActionColumnSequence.identifier(),
ActionColumnState.identifier(),
ActionColumnName.identifier(),
ActionColumnScript.identifier(),
ActionColumnTimeout.identifier(),
ActionColumnAllowedToFail.identifier(),
).From(actionTable.identifier()).PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*Action, error) {
action := new(Action)
err := row.Scan(
&action.ID,
&action.CreationDate,
&action.ChangeDate,
&action.ResourceOwner,
&action.Sequence,
&action.State,
&action.Name,
&action.Script,
&action.Timeout,
&action.AllowedToFail,
)
if err != nil {
if errs.Is(err, sql.ErrNoRows) {
return nil, errors.ThrowNotFound(err, "QUERY-GEfnb", "Errors.Action.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-Dbnt4", "Errors.Internal")
}
return action, nil
}
}

View File

@@ -2,172 +2,227 @@ package query
import (
"context"
"database/sql"
"time"
"github.com/Masterminds/squirrel"
sq "github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/query/projection"
)
func (q *Queries) GetActionsByFlowAndTriggerType(ctx context.Context, flowType domain.FlowType, triggerType domain.TriggerType) ([]*Action, error) {
flowTypeQuery, _ := NewTriggerActionFlowTypeSearchQuery(flowType)
triggerTypeQuery, _ := NewTriggerActionTriggerTypeSearchQuery(triggerType)
return q.SearchActionsFromFlow(ctx, &TriggerActionSearchQueries{Queries: []SearchQuery{flowTypeQuery, triggerTypeQuery}})
var (
flowsTriggersTable = table{
name: projection.FlowTriggerTable,
}
FlowsTriggersColumnFlowType = Column{
name: projection.FlowTypeCol,
table: flowsTriggersTable,
}
FlowsTriggersColumnTriggerType = Column{
name: projection.FlowTriggerTypeCol,
table: flowsTriggersTable,
}
FlowsTriggersColumnResourceOwner = Column{
name: projection.FlowResourceOwnerCol,
table: flowsTriggersTable,
}
FlowsTriggersColumnTriggerSequence = Column{
name: projection.FlowActionTriggerSequenceCol,
table: flowsTriggersTable,
}
FlowsTriggersColumnActionID = Column{
name: projection.FlowActionIDCol,
table: flowsTriggersTable,
}
)
type Flow struct {
ID string
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
Sequence uint64
Type domain.FlowType
TriggerActions map[domain.TriggerType][]*Action
}
var triggerActionsQuery = squirrel.StatementBuilder.Select("creation_date", "change_date", "resource_owner", "sequence", "action_id", "name", "script", "trigger_type", "trigger_sequence").
From("zitadel.projections.flows_actions_triggers").PlaceholderFormat(squirrel.Dollar)
func (q *Queries) SearchActionsFromFlow(ctx context.Context, query *TriggerActionSearchQueries) ([]*Action, error) {
stmt, args, err := query.ToQuery(triggerActionsQuery).OrderBy("flow_type", "trigger_type", "trigger_sequence").ToSql()
func (q *Queries) GetFlow(ctx context.Context, flowType domain.FlowType, orgID string) (*Flow, error) {
query, scan := q.prepareFlowQuery()
stmt, args, err := query.Where(
sq.Eq{
FlowsTriggersColumnFlowType.identifier(): flowType,
},
sq.Eq{
FlowsTriggersColumnResourceOwner.identifier(): orgID,
}).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-wQ3by", "Errors.orgs.invalid.request")
return nil, errors.ThrowInvalidArgument(err, "QUERY-HBRh3", "Errors.Query.InvalidRequest")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-M6mYN", "Errors.orgs.internal")
return nil, errors.ThrowInternal(err, "QUERY-Gg42f", "Errors.Internal")
}
actions := []*Action{}
for rows.Next() {
action := new(Action)
var triggerType domain.TriggerType
var triggerSequence int
rows.Scan(
&action.CreationDate,
&action.ChangeDate,
&action.ResourceOwner,
&action.Sequence,
//&action.State, //TODO: state in next release
&action.ID,
&action.Name,
&action.Script,
&triggerType,
&triggerSequence,
)
actions = append(actions, action)
}
if err := rows.Err(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-pA0Wj", "Errors.actions.internal")
}
return actions, nil
return scan(rows)
}
func (q *Queries) GetFlow(ctx context.Context, flowType domain.FlowType) (*Flow, error) {
flowTypeQuery, _ := NewTriggerActionFlowTypeSearchQuery(flowType)
return q.SearchFlow(ctx, &TriggerActionSearchQueries{Queries: []SearchQuery{flowTypeQuery}})
}
func (q *Queries) SearchFlow(ctx context.Context, query *TriggerActionSearchQueries) (*Flow, error) {
stmt, args, err := query.ToQuery(triggerActionsQuery.OrderBy("flow_type", "trigger_type", "trigger_sequence")).ToSql()
func (q *Queries) GetActionsByFlowAndTriggerType(ctx context.Context, flowType domain.FlowType, triggerType domain.TriggerType, orgID string) ([]*Action, error) {
stmt, scan := q.prepareTriggerActionsQuery()
query, args, err := stmt.Where(
sq.Eq{
FlowsTriggersColumnFlowType.identifier(): flowType,
},
sq.Eq{
FlowsTriggersColumnTriggerType.identifier(): triggerType,
},
sq.Eq{
FlowsTriggersColumnResourceOwner.identifier(): orgID,
}).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-wQ3by", "Errors.orgs.invalid.request")
return nil, errors.ThrowInternal(err, "QUERY-Dgff3", "Errors.Query.SQLStatement")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
rows, err := q.client.QueryContext(ctx, query, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-M6mYN", "Errors.orgs.internal")
return nil, errors.ThrowInternal(err, "QUERY-SDf52", "Errors.Internal")
}
flow := &Flow{
TriggerActions: make(map[domain.TriggerType][]*Action),
}
for rows.Next() {
action := new(Action)
var triggerType domain.TriggerType
var triggerSequence int
rows.Scan(
&action.CreationDate,
&action.ChangeDate,
&action.ResourceOwner,
&action.Sequence,
//&action.State, //TODO: state in next release
&action.ID,
&action.Name,
&action.Script,
&triggerType,
&triggerSequence,
)
flow.TriggerActions[triggerType] = append(flow.TriggerActions[triggerType], action)
}
if err := rows.Err(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-pA0Wj", "Errors.actions.internal")
}
return flow, nil
return scan(rows)
}
func (q *Queries) GetFlowTypesOfActionID(ctx context.Context, actionID string) ([]domain.FlowType, error) {
actionIDQuery, _ := NewTriggerActionActionIDSearchQuery(actionID)
query := &TriggerActionSearchQueries{Queries: []SearchQuery{actionIDQuery}}
stmt, args, err := query.ToQuery(
squirrel.StatementBuilder.
Select("flow_type").
From("zitadel.projections.flows_actions_triggers").
PlaceholderFormat(squirrel.Dollar)).ToSql()
stmt, args, err := squirrel.StatementBuilder.
Select(FlowsTriggersColumnFlowType.identifier()).
From(flowsTriggersTable.identifier()).
Where(sq.Eq{
FlowsTriggersColumnActionID.identifier(): actionID,
}).
PlaceholderFormat(squirrel.Dollar).
ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-wQ3by", "Errors.orgs.invalid.request")
return nil, errors.ThrowInvalidArgument(err, "QUERY-Dh311", "Errors.Query.InvalidRequest")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-M6mYN", "Errors.orgs.internal")
return nil, errors.ThrowInternal(err, "QUERY-Bhj4w", "Errors.Internal")
}
flowTypes := make([]domain.FlowType, 0)
for rows.Next() {
var flow_type domain.FlowType
rows.Scan(
&flow_type,
var flowType domain.FlowType
err := rows.Scan(
&flowType,
)
flowTypes = append(flowTypes, flow_type)
if err != nil {
return nil, err
}
flowTypes = append(flowTypes, flowType)
}
if err := rows.Err(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-pA0Wj", "Errors.actions.internal")
if err := rows.Close(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-Fbgnh", "Errors.Query.CloseRows")
}
return flowTypes, nil
}
type Flow struct {
ID string `col:"id"`
CreationDate time.Time `col:"creation_date"`
ChangeDate time.Time `col:"change_date"`
ResourceOwner string `col:"resource_owner"`
Sequence uint64 `col:"sequence"`
Type domain.FlowType `col:"flow_type"`
func (q *Queries) prepareTriggerActionsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*Action, error)) {
return sq.Select(
ActionColumnID.identifier(),
ActionColumnCreationDate.identifier(),
ActionColumnChangeDate.identifier(),
ActionColumnResourceOwner.identifier(),
//ActionColumnState.identifier(),
ActionColumnSequence.identifier(),
ActionColumnName.identifier(),
ActionColumnScript.identifier(),
FlowsTriggersColumnTriggerType.identifier(),
FlowsTriggersColumnTriggerSequence.identifier(),
).
From(flowsTriggersTable.name).
LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID)).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) ([]*Action, error) {
actions := make([]*Action, 0)
for rows.Next() {
action := new(Action)
var triggerType domain.TriggerType
var triggerSequence int
err := rows.Scan(
&action.ID,
&action.CreationDate,
&action.ChangeDate,
&action.ResourceOwner,
//&action.State, //TODO: state in next release
&action.Sequence,
&action.Name,
&action.Script,
&triggerType,
&triggerSequence,
)
if err != nil {
return nil, err
}
actions = append(actions, action)
}
TriggerActions map[domain.TriggerType][]*Action
if err := rows.Close(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-Df42d", "Errors.Query.CloseRows")
}
return actions, nil
}
}
type TriggerActionSearchQueries struct {
SearchRequest
Queries []SearchQuery
}
func (q *Queries) prepareFlowQuery() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) {
return sq.Select(
ActionColumnID.identifier(),
ActionColumnCreationDate.identifier(),
ActionColumnChangeDate.identifier(),
ActionColumnResourceOwner.identifier(),
//ActionColumnState.identifier(),
ActionColumnSequence.identifier(),
ActionColumnName.identifier(),
ActionColumnScript.identifier(),
FlowsTriggersColumnTriggerType.identifier(),
FlowsTriggersColumnTriggerSequence.identifier(),
).
From(flowsTriggersTable.name).
LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID)).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*Flow, error) {
flow := &Flow{
TriggerActions: make(map[domain.TriggerType][]*Action),
}
for rows.Next() {
action := new(Action)
var triggerType domain.TriggerType
var triggerSequence int
err := rows.Scan(
&action.ID,
&action.CreationDate,
&action.ChangeDate,
&action.ResourceOwner,
//&action.State, //TODO: state in next release
&action.Sequence,
&action.Name,
&action.Script,
&triggerType,
&triggerSequence,
)
if err != nil {
return nil, err
}
flow.TriggerActions[triggerType] = append(flow.TriggerActions[triggerType], action)
}
func (q *TriggerActionSearchQueries) ToQuery(query squirrel.SelectBuilder) squirrel.SelectBuilder {
query = q.SearchRequest.ToQuery(query)
for _, q := range q.Queries {
query = q.ToQuery(query)
}
return query
}
if err := rows.Close(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-Dfbe2", "Errors.Query.CloseRows")
}
func NewTriggerActionTriggerTypeSearchQuery(value domain.TriggerType) (SearchQuery, error) {
return NewIntQuery("trigger_type", int(value), IntEquals)
}
func NewTriggerActionFlowTypeSearchQuery(value domain.FlowType) (SearchQuery, error) {
return NewIntQuery("flow_type", int(value), IntEquals)
}
func NewTriggerActionActionIDSearchQuery(actionID string) (SearchQuery, error) {
return NewTextQuery("action_id", actionID, TextEquals)
return flow, nil
}
}

View File

@@ -0,0 +1,75 @@
package query
import (
"context"
"database/sql"
errs "errors"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/query/projection"
"github.com/caos/zitadel/internal/errors"
)
type LatestSequence struct {
Sequence uint64
Timestamp time.Time
}
func prepareLatestSequence() (sq.SelectBuilder, func(*sql.Row) (*LatestSequence, error)) {
return sq.Select(
CurrentSequenceColCurrentSequence.identifier(),
CurrentSequenceColTimestamp.identifier()).
From(currentSequencesTable.identifier()).PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*LatestSequence, error) {
seq := new(LatestSequence)
err := row.Scan(
&seq.Sequence,
&seq.Timestamp,
)
if err != nil {
if errs.Is(err, sql.ErrNoRows) {
return nil, errors.ThrowNotFound(err, "QUERY-gmd9o", "Errors.CurrentSequence.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-aAZ1D", "Errors.Internal")
}
return seq, nil
}
}
func (q *Queries) latestSequence(ctx context.Context, projection table) (*LatestSequence, error) {
query, scan := prepareLatestSequence()
stmt, args, err := query.Where(sq.Eq{
CurrentSequenceColProjectionName.identifier(): projection.name,
}).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-5CfX9", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, stmt, args...)
return scan(row)
}
var (
currentSequencesTable = table{
name: projection.CurrentSeqTable,
}
CurrentSequenceColAggregateType = Column{
name: "aggregate_type",
table: currentSequencesTable,
}
CurrentSequenceColCurrentSequence = Column{
name: "current_sequence",
table: currentSequencesTable,
}
CurrentSequenceColTimestamp = Column{
name: "timestamp",
table: currentSequencesTable,
}
CurrentSequenceColProjectionName = Column{
name: "projection_name",
table: currentSequencesTable,
}
)

252
internal/query/org.go Normal file
View File

@@ -0,0 +1,252 @@
package query
import (
"context"
"database/sql"
errs "errors"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/query/projection"
)
var (
orgsTable = table{
name: projection.OrgProjectionTable,
}
OrgColumnID = Column{
name: projection.OrgColumnID,
table: orgsTable,
}
OrgColumnCreationDate = Column{
name: projection.OrgColumnCreationDate,
table: orgsTable,
}
OrgColumnChangeDate = Column{
name: projection.OrgColumnChangeDate,
table: orgsTable,
}
OrgColumnResourceOwner = Column{
name: projection.OrgColumnResourceOwner,
table: orgsTable,
}
OrgColumnState = Column{
name: projection.OrgColumnState,
table: orgsTable,
}
OrgColumnSequence = Column{
name: projection.OrgColumnSequence,
table: orgsTable,
}
OrgColumnName = Column{
name: projection.OrgColumnName,
table: orgsTable,
}
OrgColumnDomain = Column{
name: projection.OrgColumnDomain,
table: orgsTable,
}
)
type Orgs struct {
SearchResponse
Orgs []*Org
}
type Org struct {
ID string
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
State domain.OrgState
Sequence uint64
Name string
Domain string
}
type OrgSearchQueries struct {
SearchRequest
Queries []SearchQuery
}
func (q *OrgSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = q.SearchRequest.toQuery(query)
for _, q := range q.Queries {
query = q.ToQuery(query)
}
return query
}
func (q *Queries) OrgByID(ctx context.Context, id string) (*Org, error) {
stmt, scan := prepareOrgQuery()
query, args, err := stmt.Where(sq.Eq{
OrgColumnID.identifier(): id,
}).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-AWx52", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, query, args...)
return scan(row)
}
func (q *Queries) OrgByDomainGlobal(ctx context.Context, domain string) (*Org, error) {
stmt, scan := prepareOrgQuery()
query, args, err := stmt.Where(sq.Eq{
OrgColumnDomain.identifier(): domain,
}).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-TYUCE", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, query, args...)
return scan(row)
}
func (q *Queries) IsOrgUnique(ctx context.Context, name, domain string) (isUnique bool, err error) {
query, scan := prepareOrgUniqueQuery()
stmt, args, err := query.Where(
sq.Or{
sq.Eq{
OrgColumnDomain.identifier(): domain,
},
sq.Eq{
OrgColumnName.identifier(): name,
},
}).ToSql()
if err != nil {
return false, errors.ThrowInternal(err, "QUERY-Dgbe2", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, stmt, args...)
return scan(row)
}
func (q *Queries) ExistsOrg(ctx context.Context, id string) (err error) {
_, err = q.OrgByID(ctx, id)
return err
}
func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries) (orgs *Orgs, err error) {
query, scan := prepareOrgsQuery()
stmt, args, err := queries.toQuery(query).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-wQ3by", "Errors.Query.InvalidRequest")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-M6mYN", "Errors.Internal")
}
orgs, err = scan(rows)
if err != nil {
return nil, err
}
orgs.LatestSequence, err = q.latestSequence(ctx, orgsTable)
return orgs, err
}
func NewOrgDomainSearchQuery(method TextComparison, value string) (SearchQuery, error) {
return NewTextQuery(OrgColumnDomain, value, method)
}
func NewOrgNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
return NewTextQuery(OrgColumnName, value, method)
}
func prepareOrgsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Orgs, error)) {
return sq.Select(
OrgColumnID.identifier(),
OrgColumnCreationDate.identifier(),
OrgColumnChangeDate.identifier(),
OrgColumnResourceOwner.identifier(),
OrgColumnState.identifier(),
OrgColumnSequence.identifier(),
OrgColumnName.identifier(),
OrgColumnDomain.identifier(),
countColumn.identifier()).
From(orgsTable.identifier()).PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*Orgs, error) {
orgs := make([]*Org, 0)
var count uint64
for rows.Next() {
org := new(Org)
err := rows.Scan(
&org.ID,
&org.CreationDate,
&org.ChangeDate,
&org.ResourceOwner,
&org.State,
&org.Sequence,
&org.Name,
&org.Domain,
&count,
)
if err != nil {
return nil, err
}
orgs = append(orgs, org)
}
if err := rows.Close(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-QMXJv", "Errors.Query.CloseRows")
}
return &Orgs{
Orgs: orgs,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
}
func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) {
return sq.Select(
OrgColumnID.identifier(),
OrgColumnCreationDate.identifier(),
OrgColumnChangeDate.identifier(),
OrgColumnResourceOwner.identifier(),
OrgColumnState.identifier(),
OrgColumnSequence.identifier(),
OrgColumnName.identifier(),
OrgColumnDomain.identifier(),
).
From(orgsTable.identifier()).PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*Org, error) {
o := new(Org)
err := row.Scan(
&o.ID,
&o.CreationDate,
&o.ChangeDate,
&o.ResourceOwner,
&o.State,
&o.Sequence,
&o.Name,
&o.Domain,
)
if err != nil {
if errs.Is(err, sql.ErrNoRows) {
return nil, errors.ThrowNotFound(err, "QUERY-iTTGJ", "Errors.Org.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-pWS5H", "Errors.Internal")
}
return o, nil
}
}
func prepareOrgUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) {
return sq.Select(uniqueColumn.identifier()).
From(orgsTable.identifier()).PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (isUnique bool, err error) {
err = row.Scan(&isUnique)
if err != nil {
return false, errors.ThrowInternal(err, "QUERY-e6EiG", "Errors.Internal")
}
return isUnique, err
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
@@ -12,13 +13,27 @@ import (
"github.com/caos/zitadel/internal/repository/action"
)
const (
ActionTable = "zitadel.projections.actions"
ActionIDCol = "id"
ActionCreationDateCol = "creation_date"
ActionChangeDateCol = "change_date"
ActionResourceOwnerCol = "resource_owner"
ActionStateCol = "action_state"
ActionSequenceCol = "sequence"
ActionNameCol = "name"
ActionScriptCol = "script"
ActionTimeoutCol = "timeout"
ActionAllowedToFailCol = "allowed_to_fail"
)
type ActionProjection struct {
crdb.StatementHandler
}
func NewActionProjection(ctx context.Context, config crdb.StatementHandlerConfig) *ActionProjection {
p := &ActionProjection{}
config.ProjectionName = "projections.actions"
config.ProjectionName = ActionTable
config.Reducers = p.reducers()
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p
@@ -54,38 +69,25 @@ func (p *ActionProjection) reducers() []handler.AggregateReducer {
}
}
const (
actionIDCol = "id"
actionCreationDateCol = "creation_date"
actionChangeDateCol = "change_date"
actionResourceOwnerCol = "resource_owner"
actionStateCol = "action_state"
actionSequenceCol = "sequence"
actionNameCol = "name"
actionScriptCol = "script"
actionTimeoutCol = "timeout"
actionAllowedToFailCol = "allowed_to_fail"
)
func (p *ActionProjection) reduceActionAdded(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.AddedEvent)
if !ok {
logging.LogWithFields("HANDL-zWCk3", "seq", event.Sequence, "expectedType", action.AddedEventType).Error("was not an event")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-uYq4r", "reduce.wrong.event.type")
logging.LogWithFields("HANDL-Sgg31", "seq", event.Sequence, "expectedType", action.AddedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-Dff21", "reduce.wrong.event.type")
}
return crdb.NewCreateStatement(
e,
[]handler.Column{
handler.NewCol(actionIDCol, e.Aggregate().ID),
handler.NewCol(actionCreationDateCol, e.CreationDate()),
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(actionSequenceCol, e.Sequence()),
handler.NewCol(actionNameCol, e.Name),
handler.NewCol(actionScriptCol, e.Script),
handler.NewCol(actionTimeoutCol, e.Timeout),
handler.NewCol(actionAllowedToFailCol, e.AllowedToFail),
handler.NewCol(actionStateCol, domain.ActionStateActive),
handler.NewCol(ActionIDCol, e.Aggregate().ID),
handler.NewCol(ActionCreationDateCol, e.CreationDate()),
handler.NewCol(ActionChangeDateCol, e.CreationDate()),
handler.NewCol(ActionResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(ActionSequenceCol, e.Sequence()),
handler.NewCol(ActionNameCol, e.Name),
handler.NewCol(ActionScriptCol, e.Script),
handler.NewCol(ActionTimeoutCol, e.Timeout),
handler.NewCol(ActionAllowedToFailCol, e.AllowedToFail),
handler.NewCol(ActionStateCol, domain.ActionStateActive),
},
), nil
}
@@ -93,30 +95,30 @@ func (p *ActionProjection) reduceActionAdded(event eventstore.EventReader) (*han
func (p *ActionProjection) reduceActionChanged(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.ChangedEvent)
if !ok {
logging.LogWithFields("HANDL-q4oq8", "seq", event.Sequence, "expected", action.ChangedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-Bg8oM", "reduce.wrong.event.type")
logging.LogWithFields("HANDL-Dg2th", "seq", event.Sequence, "expected", action.ChangedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-Gg43d", "reduce.wrong.event.type")
}
values := []handler.Column{
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionSequenceCol, e.Sequence()),
handler.NewCol(ActionChangeDateCol, e.CreationDate()),
handler.NewCol(ActionSequenceCol, e.Sequence()),
}
if e.Name != nil {
values = append(values, handler.NewCol(actionNameCol, *e.Name))
values = append(values, handler.NewCol(ActionNameCol, *e.Name))
}
if e.Script != nil {
values = append(values, handler.NewCol(actionScriptCol, *e.Script))
values = append(values, handler.NewCol(ActionScriptCol, *e.Script))
}
if e.Timeout != nil {
values = append(values, handler.NewCol(actionTimeoutCol, *e.Timeout))
values = append(values, handler.NewCol(ActionTimeoutCol, *e.Timeout))
}
if e.AllowedToFail != nil {
values = append(values, handler.NewCol(actionAllowedToFailCol, *e.AllowedToFail))
values = append(values, handler.NewCol(ActionAllowedToFailCol, *e.AllowedToFail))
}
return crdb.NewUpdateStatement(
e,
values,
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
handler.NewCond(ActionIDCol, e.Aggregate().ID),
},
), nil
}
@@ -124,18 +126,18 @@ func (p *ActionProjection) reduceActionChanged(event eventstore.EventReader) (*h
func (p *ActionProjection) reduceActionDeactivated(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.DeactivatedEvent)
if !ok {
logging.LogWithFields("HANDL-1gwdc", "seq", event.Sequence, "expectedType", action.DeactivatedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-BApK4", "reduce.wrong.event.type")
logging.LogWithFields("HANDL-Fhhjd", "seq", event.Sequence, "expectedType", action.DeactivatedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-Fgh32", "reduce.wrong.event.type")
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionSequenceCol, e.Sequence()),
handler.NewCol(actionStateCol, domain.ActionStateInactive),
handler.NewCol(ActionChangeDateCol, e.CreationDate()),
handler.NewCol(ActionSequenceCol, e.Sequence()),
handler.NewCol(ActionStateCol, domain.ActionStateInactive),
},
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
handler.NewCond(ActionIDCol, e.Aggregate().ID),
},
), nil
}
@@ -143,18 +145,18 @@ func (p *ActionProjection) reduceActionDeactivated(event eventstore.EventReader)
func (p *ActionProjection) reduceActionReactivated(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.ReactivatedEvent)
if !ok {
logging.LogWithFields("HANDL-Vjwiy", "seq", event.Sequence, "expectedType", action.ReactivatedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-o37De", "reduce.wrong.event.type")
logging.LogWithFields("HANDL-Fg4r3", "seq", event.Sequence, "expectedType", action.ReactivatedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-hwdqa", "reduce.wrong.event.type")
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionSequenceCol, e.Sequence()),
handler.NewCol(actionStateCol, domain.ActionStateActive),
handler.NewCol(ActionChangeDateCol, e.CreationDate()),
handler.NewCol(ActionSequenceCol, e.Sequence()),
handler.NewCol(ActionStateCol, domain.ActionStateActive),
},
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
handler.NewCond(ActionIDCol, e.Aggregate().ID),
},
), nil
}
@@ -162,13 +164,13 @@ func (p *ActionProjection) reduceActionReactivated(event eventstore.EventReader)
func (p *ActionProjection) reduceActionRemoved(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.RemovedEvent)
if !ok {
logging.LogWithFields("HANDL-79OhB", "seq", event.Sequence, "expectedType", action.RemovedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-4TbKT", "reduce.wrong.event.type")
logging.LogWithFields("HANDL-Dgwh2", "seq", event.Sequence, "expectedType", action.RemovedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-Dgh2d", "reduce.wrong.event.type")
}
return crdb.NewDeleteStatement(
e,
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
handler.NewCond(ActionIDCol, e.Aggregate().ID),
},
), nil
}

View File

@@ -0,0 +1,178 @@
package projection
import (
"testing"
"time"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/repository/action"
)
func TestActionProjection_reduces(t *testing.T) {
type args struct {
event func(t *testing.T) eventstore.EventReader
}
tests := []struct {
name string
args args
reduce func(event eventstore.EventReader) (*handler.Statement, error)
want wantReduce
}{
{
name: "reduceActionAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(action.AddedEventType),
action.AggregateType,
[]byte(`{"name": "name", "script":"name(){}","timeout": 3000000000, "allowedToFail": true}`),
), action.AddedEventMapper),
},
reduce: (&ActionProjection{}).reduceActionAdded,
want: wantReduce{
projection: ActionTable,
aggregateType: eventstore.AggregateType("action"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "INSERT INTO zitadel.projections.actions (id, creation_date, change_date, resource_owner, sequence, name, script, timeout, allowed_to_fail, action_state) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
expectedArgs: []interface{}{
"agg-id",
anyArg{},
anyArg{},
"ro-id",
uint64(15),
"name",
"name(){}",
3 * time.Second,
true,
domain.ActionStateActive,
},
},
},
},
{
name: "reduceActionChanged",
args: args{
event: getEvent(testEvent(
repository.EventType(action.ChangedEventType),
action.AggregateType,
[]byte(`{"name": "name2", "script":"name2(){}"}`),
), action.ChangedEventMapper),
},
reduce: (&ActionProjection{}).reduceActionChanged,
want: wantReduce{
projection: ActionTable,
aggregateType: eventstore.AggregateType("action"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "UPDATE zitadel.projections.actions SET (change_date, sequence, name, script) = ($1, $2, $3, $4) WHERE (id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
"name2",
"name2(){}",
"agg-id",
},
},
},
},
{
name: "reduceActionDeactivated",
args: args{
event: getEvent(testEvent(
repository.EventType(action.ChangedEventType),
action.AggregateType,
[]byte(`{}`),
), action.DeactivatedEventMapper),
},
reduce: (&ActionProjection{}).reduceActionDeactivated,
want: wantReduce{
projection: ActionTable,
aggregateType: eventstore.AggregateType("action"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "UPDATE zitadel.projections.actions SET (change_date, sequence, action_state) = ($1, $2, $3) WHERE (id = $4)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
domain.ActionStateInactive,
"agg-id",
},
},
},
},
{
name: "reduceActionReactivated",
args: args{
event: getEvent(testEvent(
repository.EventType(action.ChangedEventType),
action.AggregateType,
[]byte(`{}`),
), action.ReactivatedEventMapper),
},
reduce: (&ActionProjection{}).reduceActionReactivated,
want: wantReduce{
projection: ActionTable,
aggregateType: eventstore.AggregateType("action"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "UPDATE zitadel.projections.actions SET (change_date, sequence, action_state) = ($1, $2, $3) WHERE (id = $4)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
domain.ActionStateActive,
"agg-id",
},
},
},
},
{
name: "reduceActionRemoved",
args: args{
event: getEvent(testEvent(
repository.EventType(action.ChangedEventType),
action.AggregateType,
[]byte(`{}`),
), action.RemovedEventMapper),
},
reduce: (&ActionProjection{}).reduceActionRemoved,
want: wantReduce{
projection: ActionTable,
aggregateType: eventstore.AggregateType("action"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "DELETE FROM zitadel.projections.actions WHERE (id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := baseEvent(t)
got, err := tt.reduce(event)
if _, ok := err.(errors.InvalidArgument); !ok {
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
}
event = tt.args.event(t)
got, err = tt.reduce(event)
assertReduce(t, got, err, tt.want)
})
}
}

View File

@@ -9,6 +9,7 @@ type Config struct {
BulkLimit uint64
CRDB types.SQL
Customizations map[string]CustomConfig
MaxIterators int
}
type CustomConfig struct {

View File

@@ -0,0 +1,93 @@
package projection
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/handler/crdb"
"github.com/caos/zitadel/internal/repository/org"
)
const (
FlowTriggerTable = "zitadel.projections.flows_triggers"
FlowTypeCol = "flow_type"
FlowTriggerTypeCol = "trigger_type"
FlowResourceOwnerCol = "resource_owner"
FlowActionTriggerSequenceCol = "trigger_sequence"
FlowActionIDCol = "action_id"
)
type FlowProjection struct {
crdb.StatementHandler
}
func NewFlowProjection(ctx context.Context, config crdb.StatementHandlerConfig) *FlowProjection {
p := &FlowProjection{}
config.ProjectionName = FlowTriggerTable
config.Reducers = p.reducers()
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p
}
func (p *FlowProjection) reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: org.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: org.TriggerActionsSetEventType,
Reduce: p.reduceTriggerActionsSetEventType,
},
{
Event: org.FlowClearedEventType,
Reduce: p.reduceFlowClearedEventType,
},
},
},
}
}
func (p *FlowProjection) reduceTriggerActionsSetEventType(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.TriggerActionsSetEvent)
if !ok {
logging.LogWithFields("HANDL-zWCk3", "seq", event.Sequence, "expectedType", org.TriggerActionsSetEventType).Error("was not an trigger actions set event")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-uYq4r", "reduce.wrong.event.type")
}
stmts := make([]func(reader eventstore.EventReader) crdb.Exec, len(e.ActionIDs)+1)
stmts[0] = crdb.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(FlowTypeCol, e.FlowType),
handler.NewCond(FlowTriggerTypeCol, e.TriggerType),
},
)
for i, id := range e.ActionIDs {
stmts[i+1] = crdb.AddCreateStatement(
[]handler.Column{
handler.NewCol(FlowResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(FlowTypeCol, e.FlowType),
handler.NewCol(FlowTriggerTypeCol, e.TriggerType),
handler.NewCol(FlowActionIDCol, id),
handler.NewCol(FlowActionTriggerSequenceCol, i),
},
)
}
return crdb.NewMultiStatement(e, stmts...), nil
}
func (p *FlowProjection) reduceFlowClearedEventType(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.FlowClearedEvent)
if !ok {
logging.LogWithFields("HANDL-zWCk3", "seq", event.Sequence, "expectedType", org.FlowClearedEventType).Error("was not a flow cleared event")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-uYq4r", "reduce.wrong.event.type")
}
return crdb.NewDeleteStatement(
e,
[]handler.Condition{
handler.NewCond(FlowTypeCol, e.FlowType),
},
), nil
}

View File

@@ -0,0 +1,93 @@
package projection
import (
"testing"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/repository/org"
)
func TestFlowProjection_reduces(t *testing.T) {
type args struct {
event func(t *testing.T) eventstore.EventReader
}
tests := []struct {
name string
args args
reduce func(event eventstore.EventReader) (*handler.Statement, error)
want wantReduce
}{
//TODO: multi stmt tests
//{
// name: "reduceTriggerActionsSetEventType",
// args: args{
// event: getEvent(testEvent(
// repository.EventType(org.TriggerActionsSetEventType),
// org.AggregateType,
// []byte(`{"flowType": 1, "triggerType": 1, "actionIDs": ["id1", "id2"]}`),
// ), org.TriggerActionsSetEventMapper),
// },
// reduce: (&FlowProjection{}).reduceTriggerActionsSetEventType,
// want: wantReduce{
// projection: FlowTriggerTable,
// aggregateType: eventstore.AggregateType("org"),
// sequence: 15,
// previousSequence: 10,
// executer: &testExecuter{
// shouldExec: true,
// expectedStmt: "DELETE FROM zitadel.projections.actions WHERE (flow_type, trigger_type) = ($1, $2); INSERT INTO zitadel.projections.actions (resource_owner, flow_type, trigger_type, action_id, trigger_sequence) = ($3, $1, $2, $4, $5); INSERT INTO zitadel.projections.actions (resource_owner, flow_type, trigger_type, action_id, trigger_sequence) = ($3, $1, $2, $6, $7)",
// expectedArgs: []interface{}{
// domain.FlowTypeExternalAuthentication,
// domain.TriggerTypePostAuthentication,
// "ro-id",
// "id1",
// 0,
// "id2",
// 1,
// },
// },
// },
//},
{
name: "reduceFlowClearedEventType",
args: args{
event: getEvent(testEvent(
repository.EventType(org.FlowClearedEventType),
org.AggregateType,
[]byte(`{"flowType": 1}`),
), org.FlowClearedEventMapper),
},
reduce: (&FlowProjection{}).reduceFlowClearedEventType,
want: wantReduce{
projection: FlowTriggerTable,
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "DELETE FROM zitadel.projections.flows_triggers WHERE (flow_type = $1)",
expectedArgs: []interface{}{
domain.FlowTypeExternalAuthentication,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := baseEvent(t)
got, err := tt.reduce(event)
if _, ok := err.(errors.InvalidArgument); !ok {
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
}
event = tt.args.event(t)
got, err = tt.reduce(event)
assertReduce(t, got, err, tt.want)
})
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
@@ -16,9 +17,13 @@ type OrgProjection struct {
crdb.StatementHandler
}
const (
OrgProjectionTable = "zitadel.projections.orgs"
)
func NewOrgProjection(ctx context.Context, config crdb.StatementHandlerConfig) *OrgProjection {
p := &OrgProjection{}
config.ProjectionName = "projections.orgs"
config.ProjectionName = OrgProjectionTable
config.Reducers = p.reducers()
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p
@@ -54,33 +59,35 @@ func (p *OrgProjection) reducers() []handler.AggregateReducer {
}
}
type OrgColumn string
const (
orgIDCol = "id"
orgCreationDateCol = "creation_date"
orgChangeDateCol = "change_date"
orgResourceOwnerCol = "resource_owner"
orgStateCol = "org_state"
orgSequenceCol = "sequence"
orgDomainCol = "domain"
orgNameCol = "name"
OrgColumnID = "id"
OrgColumnCreationDate = "creation_date"
OrgColumnChangeDate = "change_date"
OrgColumnResourceOwner = "resource_owner"
OrgColumnState = "org_state"
OrgColumnSequence = "sequence"
OrgColumnName = "name"
OrgColumnDomain = "primary_domain"
)
func (p *OrgProjection) reduceOrgAdded(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.OrgAddedEvent)
if !ok {
logging.LogWithFields("HANDL-zWCk3", "seq", event.Sequence, "expectedType", org.OrgAddedEventType).Error("was not an event")
logging.LogWithFields("HANDL-zWCk3", "seq", event.Sequence(), "expectedType", org.OrgAddedEventType).Error("was not an event")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-uYq4r", "reduce.wrong.event.type")
}
return crdb.NewCreateStatement(
e,
[]handler.Column{
handler.NewCol(orgIDCol, e.Aggregate().ID),
handler.NewCol(orgCreationDateCol, e.CreationDate()),
handler.NewCol(orgChangeDateCol, e.CreationDate()),
handler.NewCol(orgResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(orgSequenceCol, e.Sequence()),
handler.NewCol(orgNameCol, e.Name),
handler.NewCol(orgStateCol, domain.OrgStateActive),
handler.NewCol(OrgColumnID, e.Aggregate().ID),
handler.NewCol(OrgColumnCreationDate, e.CreationDate()),
handler.NewCol(OrgColumnChangeDate, e.CreationDate()),
handler.NewCol(OrgColumnResourceOwner, e.Aggregate().ResourceOwner),
handler.NewCol(OrgColumnSequence, e.Sequence()),
handler.NewCol(OrgColumnName, e.Name),
handler.NewCol(OrgColumnState, domain.OrgStateActive),
},
), nil
}
@@ -88,21 +95,21 @@ func (p *OrgProjection) reduceOrgAdded(event eventstore.EventReader) (*handler.S
func (p *OrgProjection) reduceOrgChanged(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.OrgChangedEvent)
if !ok {
logging.LogWithFields("HANDL-q4oq8", "seq", event.Sequence, "expected", org.OrgChangedEventType).Error("wrong event type")
logging.LogWithFields("HANDL-q4oq8", "seq", event.Sequence(), "expected", org.OrgChangedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-Bg8oM", "reduce.wrong.event.type")
}
values := []handler.Column{
handler.NewCol(orgChangeDateCol, e.CreationDate()),
handler.NewCol(orgSequenceCol, e.Sequence()),
}
if e.Name != "" {
values = append(values, handler.NewCol(orgNameCol, e.Name))
if e.Name == "" {
return crdb.NewNoOpStatement(e), nil
}
return crdb.NewUpdateStatement(
e,
values,
[]handler.Column{
handler.NewCol(OrgColumnChangeDate, e.CreationDate()),
handler.NewCol(OrgColumnSequence, e.Sequence()),
handler.NewCol(OrgColumnName, e.Name),
},
[]handler.Condition{
handler.NewCond(orgIDCol, e.Aggregate().ID),
handler.NewCond(OrgColumnID, e.Aggregate().ID),
},
), nil
}
@@ -110,18 +117,18 @@ func (p *OrgProjection) reduceOrgChanged(event eventstore.EventReader) (*handler
func (p *OrgProjection) reduceOrgDeactivated(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.OrgDeactivatedEvent)
if !ok {
logging.LogWithFields("HANDL-1gwdc", "seq", event.Sequence, "expectedType", org.OrgDeactivatedEventType).Error("wrong event type")
logging.LogWithFields("HANDL-1gwdc", "seq", event.Sequence(), "expectedType", org.OrgDeactivatedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-BApK4", "reduce.wrong.event.type")
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(orgChangeDateCol, e.CreationDate()),
handler.NewCol(orgSequenceCol, e.Sequence()),
handler.NewCol(orgStateCol, domain.OrgStateInactive),
handler.NewCol(OrgColumnChangeDate, e.CreationDate()),
handler.NewCol(OrgColumnSequence, e.Sequence()),
handler.NewCol(OrgColumnState, domain.OrgStateInactive),
},
[]handler.Condition{
handler.NewCond(orgIDCol, e.Aggregate().ID),
handler.NewCond(OrgColumnID, e.Aggregate().ID),
},
), nil
}
@@ -129,18 +136,18 @@ func (p *OrgProjection) reduceOrgDeactivated(event eventstore.EventReader) (*han
func (p *OrgProjection) reduceOrgReactivated(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.OrgReactivatedEvent)
if !ok {
logging.LogWithFields("HANDL-Vjwiy", "seq", event.Sequence, "expectedType", org.OrgReactivatedEventType).Error("wrong event type")
logging.LogWithFields("HANDL-Vjwiy", "seq", event.Sequence(), "expectedType", org.OrgReactivatedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-o37De", "reduce.wrong.event.type")
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(orgChangeDateCol, e.CreationDate()),
handler.NewCol(orgSequenceCol, e.Sequence()),
handler.NewCol(orgStateCol, domain.OrgStateActive),
handler.NewCol(OrgColumnChangeDate, e.CreationDate()),
handler.NewCol(OrgColumnSequence, e.Sequence()),
handler.NewCol(OrgColumnState, domain.OrgStateActive),
},
[]handler.Condition{
handler.NewCond(orgIDCol, e.Aggregate().ID),
handler.NewCond(OrgColumnID, e.Aggregate().ID),
},
), nil
}
@@ -148,18 +155,18 @@ func (p *OrgProjection) reduceOrgReactivated(event eventstore.EventReader) (*han
func (p *OrgProjection) reducePrimaryDomainSet(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.DomainPrimarySetEvent)
if !ok {
logging.LogWithFields("HANDL-79OhB", "seq", event.Sequence, "expectedType", org.OrgDomainPrimarySetEventType).Error("wrong event type")
logging.LogWithFields("HANDL-79OhB", "seq", event.Sequence(), "expectedType", org.OrgDomainPrimarySetEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-4TbKT", "reduce.wrong.event.type")
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(orgChangeDateCol, e.CreationDate()),
handler.NewCol(orgSequenceCol, e.Sequence()),
handler.NewCol(orgDomainCol, e.Domain),
handler.NewCol(OrgColumnChangeDate, e.CreationDate()),
handler.NewCol(OrgColumnSequence, e.Sequence()),
handler.NewCol(OrgColumnDomain, e.Domain),
},
[]handler.Condition{
handler.NewCond(orgIDCol, e.Aggregate().ID),
handler.NewCond(OrgColumnID, e.Aggregate().ID),
},
), nil
}

View File

@@ -16,15 +16,15 @@ import (
)
type OrgOwner struct {
OrgID string `col:"org_id"`
OrgName string `col:"org_name"`
OrgCreationDate time.Time `col:"org_creation_date"`
OwnerID string `col:"owner_id"`
OwnerLanguage *language.Tag `col:"owner_language"`
OwnerEmailAddress string `col:"owner_email"`
OwnerFirstName string `col:"owner_first_name"`
OwnerLastName string `col:"owner_last_name"`
OwnerGender domain.Gender `col:"owner_gender"`
OrgID string
OrgName string
OrgCreationDate time.Time
OwnerID string
OwnerLanguage *language.Tag
OwnerEmailAddress string
OwnerFirstName string
OwnerLastName string
OwnerGender domain.Gender
}
type OrgOwnerProjection struct {
@@ -49,7 +49,7 @@ const (
func NewOrgOwnerProjection(ctx context.Context, config crdb.StatementHandlerConfig) *OrgOwnerProjection {
p := &OrgOwnerProjection{}
config.ProjectionName = "projections.org_owners"
config.ProjectionName = "zitadel.projections.org_owners"
config.Reducers = p.reducers()
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p

View File

@@ -0,0 +1,196 @@
package projection
import (
"testing"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/repository/org"
)
func TestOrgProjection_reduces(t *testing.T) {
type args struct {
event func(t *testing.T) eventstore.EventReader
}
tests := []struct {
name string
args args
reduce func(event eventstore.EventReader) (*handler.Statement, error)
want wantReduce
}{
{
name: "reducePrimaryDomainSet",
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgDomainPrimarySetEventType),
org.AggregateType,
[]byte(`{"domain": "domain.new"}`),
), org.DomainPrimarySetEventMapper),
},
reduce: (&OrgProjection{}).reducePrimaryDomainSet,
want: wantReduce{
projection: OrgProjectionTable,
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "UPDATE zitadel.projections.orgs SET (change_date, sequence, primary_domain) = ($1, $2, $3) WHERE (id = $4)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
"domain.new",
"agg-id",
},
},
},
},
{
name: "reduceOrgReactivated",
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgReactivatedEventType),
org.AggregateType,
nil,
), org.OrgReactivatedEventMapper),
},
reduce: (&OrgProjection{}).reduceOrgReactivated,
want: wantReduce{
projection: OrgProjectionTable,
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "UPDATE zitadel.projections.orgs SET (change_date, sequence, org_state) = ($1, $2, $3) WHERE (id = $4)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
domain.OrgStateActive,
"agg-id",
},
},
},
},
{
name: "reduceOrgDeactivated",
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgDeactivatedEventType),
org.AggregateType,
nil,
), org.OrgDeactivatedEventMapper),
},
reduce: (&OrgProjection{}).reduceOrgDeactivated,
want: wantReduce{
projection: OrgProjectionTable,
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "UPDATE zitadel.projections.orgs SET (change_date, sequence, org_state) = ($1, $2, $3) WHERE (id = $4)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
domain.OrgStateInactive,
"agg-id",
},
},
},
},
{
name: "reduceOrgChanged",
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgChangedEventType),
org.AggregateType,
[]byte(`{"name": "new name"}`),
), org.OrgChangedEventMapper),
},
reduce: (&OrgProjection{}).reduceOrgChanged,
want: wantReduce{
projection: OrgProjectionTable,
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "UPDATE zitadel.projections.orgs SET (change_date, sequence, name) = ($1, $2, $3) WHERE (id = $4)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
"new name",
"agg-id",
},
},
},
},
{
name: "reduceOrgChanged no changes",
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgChangedEventType),
org.AggregateType,
[]byte(`{}`),
), org.OrgChangedEventMapper),
},
reduce: (&OrgProjection{}).reduceOrgChanged,
want: wantReduce{
projection: OrgProjectionTable,
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: false,
},
},
},
{
name: "reduceOrgAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgAddedEventType),
org.AggregateType,
[]byte(`{"name": "name"}`),
), org.OrgAddedEventMapper),
},
reduce: (&OrgProjection{}).reduceOrgAdded,
want: wantReduce{
projection: OrgProjectionTable,
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
shouldExec: true,
expectedStmt: "INSERT INTO zitadel.projections.orgs (id, creation_date, change_date, resource_owner, sequence, name, org_state) VALUES ($1, $2, $3, $4, $5, $6, $7)",
expectedArgs: []interface{}{
"agg-id",
anyArg{},
anyArg{},
"ro-id",
uint64(15),
"name",
domain.OrgStateActive,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := baseEvent(t)
got, err := tt.reduce(event)
if _, ok := err.(errors.InvalidArgument); !ok {
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
}
event = tt.args.event(t)
got, err = tt.reduce(event)
assertReduce(t, got, err, tt.want)
})
}
}

View File

@@ -15,7 +15,7 @@ type ProjectProjection struct {
func NewProjectProjection(ctx context.Context, config crdb.StatementHandlerConfig) *ProjectProjection {
p := &ProjectProjection{}
config.ProjectionName = "projections.projects"
config.ProjectionName = "zitadel.projections.projects"
config.Reducers = p.reducers()
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p

View File

@@ -2,25 +2,21 @@ package projection
import (
"context"
"database/sql"
"time"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/handler/crdb"
"github.com/caos/zitadel/internal/query/projection/flow"
)
const (
currentSeqTable = "projections.current_sequences"
CurrentSeqTable = "projections.current_sequences"
locksTable = "projections.locks"
failedEventsTable = "projections.failed_events"
)
func Start(ctx context.Context, es *eventstore.Eventstore, config Config) error {
sqlClient, err := config.CRDB.Start()
if err != nil {
return err
}
func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, config Config) error {
projectionConfig := crdb.StatementHandlerConfig{
ProjectionHandlerConfig: handler.ProjectionHandlerConfig{
HandlerConfig: handler.HandlerConfig{
@@ -30,19 +26,18 @@ func Start(ctx context.Context, es *eventstore.Eventstore, config Config) error
RetryFailedAfter: config.RetryFailedAfter.Duration,
},
Client: sqlClient,
SequenceTable: currentSeqTable,
SequenceTable: CurrentSeqTable,
LockTable: locksTable,
FailedEventsTable: failedEventsTable,
MaxFailureCount: config.MaxFailureCount,
BulkLimit: config.BulkLimit,
}
// turned off for this release
//NewOrgProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["orgs"]))
NewOrgProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["orgs"]))
//NewProjectProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["projects"]))
//owner.NewOrgOwnerProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["org_owners"]))
NewActionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["actions"]))
flow.NewFlowProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["flows"]))
NewFlowProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["flows"]))
return nil
}
@@ -62,3 +57,20 @@ func applyCustomConfig(config crdb.StatementHandlerConfig, customConfig CustomCo
return config
}
func iteratorPool(workerCount int) chan func() {
if workerCount <= 0 {
return nil
}
queue := make(chan func())
for i := 0; i < workerCount; i++ {
go func() {
for iteration := range queue {
iteration()
time.Sleep(2 * time.Second)
}
}()
}
return queue
}

View File

@@ -0,0 +1,87 @@
package projection
import (
"testing"
"time"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/repository"
)
func testEvent(
eventType repository.EventType,
aggregateType repository.AggregateType,
data []byte,
) *repository.Event {
return &repository.Event{
Sequence: 15,
PreviousAggregateSequence: 10,
PreviousAggregateTypeSequence: 10,
CreationDate: time.Now(),
Type: eventType,
AggregateType: aggregateType,
Data: data,
Version: "v1",
AggregateID: "agg-id",
ResourceOwner: "ro-id",
ID: "event-id",
EditorService: "editor-svc",
EditorUser: "editor-user",
}
}
func baseEvent(*testing.T) eventstore.EventReader {
return &eventstore.BaseEvent{}
}
func getEvent(event *repository.Event, mapper func(*repository.Event) (eventstore.EventReader, error)) func(t *testing.T) eventstore.EventReader {
return func(t *testing.T) eventstore.EventReader {
e, err := mapper(event)
if err != nil {
t.Fatalf("mapper failed: %v", err)
}
return e
}
}
type wantReduce struct {
projection string
aggregateType eventstore.AggregateType
sequence uint64
previousSequence uint64
executer *testExecuter
err func(error) bool
}
func assertReduce(t *testing.T, stmt *handler.Statement, err error, want wantReduce) {
t.Helper()
if want.err == nil && err != nil {
t.Errorf("unexpected error of type %T: %v", err, err)
return
}
if want.err != nil && want.err(err) {
return
}
if stmt.AggregateType != want.aggregateType {
t.Errorf("wront aggregate type: want: %q got: %q", want.aggregateType, stmt.AggregateType)
}
if stmt.PreviousSequence != want.previousSequence {
t.Errorf("wront previous sequence: want: %d got: %d", want.previousSequence, stmt.PreviousSequence)
}
if stmt.Sequence != want.sequence {
t.Errorf("wront sequence: want: %d got: %d", want.sequence, stmt.Sequence)
}
if stmt.Execute == nil {
want.executer.Validate(t)
return
}
err = stmt.Execute(want.executer, want.projection)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
want.executer.Validate(t)
}

View File

@@ -0,0 +1,48 @@
package projection
import (
"database/sql"
"testing"
)
type testExecuter struct {
expectedStmt string
gottenStmt string
shouldExec bool
expectedArgs []interface{}
gottenArgs []interface{}
gotExecuted bool
}
type anyArg struct{}
func (e *testExecuter) Exec(stmt string, args ...interface{}) (sql.Result, error) {
e.gottenStmt = stmt
e.gottenArgs = args
e.gotExecuted = true
return nil, nil
}
func (e *testExecuter) Validate(t *testing.T) {
t.Helper()
if e.shouldExec != e.gotExecuted {
t.Error("expected to be executed")
return
}
if len(e.gottenArgs) != len(e.expectedArgs) {
t.Errorf("wrong arg len expected: %d got: %d", len(e.expectedArgs), len(e.gottenArgs))
} else {
for i := 0; i < len(e.expectedArgs); i++ {
if _, ok := e.expectedArgs[i].(anyArg); ok {
continue
}
if e.expectedArgs[i] != e.gottenArgs[i] {
t.Errorf("wrong argument at index %d: got: %v want: %v", i, e.gottenArgs[i], e.expectedArgs[i])
}
}
}
if e.gottenStmt != e.expectedStmt {
t.Errorf("wrong stmt want:\n%s\ngot:\n%s", e.expectedStmt, e.gottenStmt)
}
}

View File

@@ -6,10 +6,8 @@ import (
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/query/projection"
"github.com/caos/zitadel/internal/repository/action"
iam_repo "github.com/caos/zitadel/internal/repository/iam"
@@ -20,12 +18,9 @@ import (
)
type Queries struct {
iamID string
eventstore *eventstore.Eventstore
idGenerator id.Generator
secretCrypto crypto.Crypto
client *sql.DB
iamID string
eventstore *eventstore.Eventstore
client *sql.DB
}
type Config struct {
@@ -39,10 +34,9 @@ func StartQueries(ctx context.Context, es *eventstore.Eventstore, projections pr
}
repo = &Queries{
iamID: defaults.IamID,
eventstore: es,
idGenerator: id.SonyFlakeGenerator,
client: sqlClient,
iamID: defaults.IamID,
eventstore: es,
client: sqlClient,
}
iam_repo.RegisterEventMappers(repo.eventstore)
usr_repo.RegisterEventMappers(repo.eventstore)
@@ -50,12 +44,7 @@ func StartQueries(ctx context.Context, es *eventstore.Eventstore, projections pr
project.RegisterEventMappers(repo.eventstore)
action.RegisterEventMappers(repo.eventstore)
repo.secretCrypto, err = crypto.NewAESCrypto(defaults.IDPConfigVerificationKey)
if err != nil {
return nil, err
}
err = projection.Start(ctx, es, projections)
err = projection.Start(ctx, sqlClient, es, projections)
if err != nil {
return nil, err
}

View File

@@ -2,19 +2,27 @@ package query
import (
"errors"
"strings"
"reflect"
sq "github.com/Masterminds/squirrel"
"github.com/lib/pq"
"github.com/caos/zitadel/internal/domain"
)
type SearchResponse struct {
Count uint64
*LatestSequence
}
type SearchRequest struct {
Offset uint64
Limit uint64
SortingColumn string
SortingColumn Column
Asc bool
}
func (req *SearchRequest) ToQuery(query sq.SelectBuilder) sq.SelectBuilder {
func (req *SearchRequest) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
if req.Offset > 0 {
query = query.Offset(req.Offset)
}
@@ -22,12 +30,12 @@ func (req *SearchRequest) ToQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = query.Limit(req.Limit)
}
if req.SortingColumn != "" {
clause := "LOWER(?)"
if !req.SortingColumn.isZero() {
clause := "LOWER(" + sqlPlaceholder + ")"
if !req.Asc {
clause += " DESC"
}
query.OrderByClause(clause, req.SortingColumn)
query = query.OrderByClause(clause, req.SortingColumn.identifier())
}
return query
@@ -40,110 +48,232 @@ type SearchQuery interface {
}
type TextQuery struct {
Column string
Column Column
Text string
Compare TextComparison
}
func NewTextQuery(column, value string, compare TextComparison) (*TextQuery, error) {
if compare < 0 || compare >= textMax {
return nil, errors.New("invalid compare")
var (
ErrInvalidCompare = errors.New("invalid compare")
ErrMissingColumn = errors.New("missing column")
ErrInvalidNumber = errors.New("value is no number")
)
func NewTextQuery(col Column, value string, compare TextComparison) (*TextQuery, error) {
if compare < 0 || compare >= textCompareMax {
return nil, ErrInvalidCompare
}
if column == "" {
return nil, errors.New("missing column")
if col.isZero() {
return nil, ErrMissingColumn
}
return &TextQuery{
Column: column,
Column: col,
Text: value,
Compare: compare,
}, nil
}
func (q *TextQuery) ToQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = query.Where(q.comp())
return query
where, args := q.comp()
return query.Where(where, args...)
}
func (s *TextQuery) comp() map[string]interface{} {
func (s *TextQuery) comp() (comparison interface{}, args []interface{}) {
switch s.Compare {
case TextEquals:
return sq.Eq{s.Column: s.Text}
case TextEqualsIgnore:
return sq.Eq{"LOWER(" + s.Column + ")": strings.ToLower(s.Text)}
return sq.Eq{s.Column.identifier(): s.Text}, nil
case TextEqualsIgnoreCase:
return sq.ILike{s.Column.identifier(): s.Text}, nil
case TextStartsWith:
return sq.Like{s.Column: s.Text + sqlPlaceholder}
case TextStartsWithIgnore:
return sq.Like{"LOWER(" + s.Column + ")": strings.ToLower(s.Text) + sqlPlaceholder}
return sq.Like{s.Column.identifier(): s.Text + "%"}, nil
case TextStartsWithIgnoreCase:
return sq.ILike{s.Column.identifier(): s.Text + "%"}, nil
case TextEndsWith:
return sq.Like{s.Column: sqlPlaceholder + s.Text}
case TextEndsWithIgnore:
return sq.Like{"LOWER(" + s.Column + ")": sqlPlaceholder + strings.ToLower(s.Text)}
return sq.Like{s.Column.identifier(): "%" + s.Text}, nil
case TextEndsWithIgnoreCase:
return sq.ILike{s.Column.identifier(): "%" + s.Text}, nil
case TextContains:
return sq.Like{s.Column: sqlPlaceholder + s.Text + sqlPlaceholder}
case TextContainsIgnore:
return sq.Like{"LOWER(" + s.Column + ")": sqlPlaceholder + strings.ToLower(s.Text) + sqlPlaceholder}
return sq.Like{s.Column.identifier(): "%" + s.Text + "%"}, nil
case TextContainsIgnoreCase:
return sq.ILike{s.Column.identifier(): "%" + s.Text + "%"}, nil
case TextListContains:
return s.Column.identifier() + " @> ? ", []interface{}{pq.StringArray{s.Text}}
}
return nil
return nil, nil
}
type TextComparison int
const (
TextEquals TextComparison = iota
TextEqualsIgnore
TextEqualsIgnoreCase
TextStartsWith
TextStartsWithIgnore
TextStartsWithIgnoreCase
TextEndsWith
TextEndsWithIgnore
TextEndsWithIgnoreCase
TextContains
TextContainsIgnore
TextContainsIgnoreCase
TextListContains
textMax
textCompareMax
)
type IntQuery struct {
Column string
Int int
Compare IntComparison
//Deprecated: Use TextComparison, will be removed as soon as all calls are changed to query
func TextComparisonFromMethod(m domain.SearchMethod) TextComparison {
switch m {
case domain.SearchMethodEquals:
return TextEquals
case domain.SearchMethodEqualsIgnoreCase:
return TextEqualsIgnoreCase
case domain.SearchMethodStartsWith:
return TextStartsWith
case domain.SearchMethodStartsWithIgnoreCase:
return TextStartsWithIgnoreCase
case domain.SearchMethodContains:
return TextContains
case domain.SearchMethodContainsIgnoreCase:
return TextContainsIgnoreCase
case domain.SearchMethodEndsWith:
return TextEndsWith
case domain.SearchMethodEndsWithIgnoreCase:
return TextEndsWithIgnoreCase
case domain.SearchMethodListContains:
return TextListContains
default:
return textCompareMax
}
}
func NewIntQuery(column string, value int, compare IntComparison) (*IntQuery, error) {
if compare < 0 || compare >= intMax {
return nil, errors.New("invalid compare")
type NumberQuery struct {
Column Column
Number interface{}
Compare NumberComparison
}
func NewNumberQuery(c Column, value interface{}, compare NumberComparison) (*NumberQuery, error) {
if compare < 0 || compare >= numberCompareMax {
return nil, ErrInvalidCompare
}
if column == "" {
return nil, errors.New("missing column")
if c.isZero() {
return nil, ErrMissingColumn
}
return &IntQuery{
Column: column,
Int: value,
switch reflect.TypeOf(value).Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
//everything fine
default:
return nil, ErrInvalidNumber
}
return &NumberQuery{
Column: c,
Number: value,
Compare: compare,
}, nil
}
func (q *IntQuery) ToQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = query.Where(q.comp())
return query
func (q *NumberQuery) ToQuery(query sq.SelectBuilder) sq.SelectBuilder {
where, args := q.comp()
return query.Where(where, args...)
}
func (s *IntQuery) comp() sq.Sqlizer {
func (s *NumberQuery) comp() (comparison interface{}, args []interface{}) {
switch s.Compare {
case IntEquals:
return sq.Eq{s.Column: s.Int}
case IntGreater:
return sq.Gt{s.Column: s.Int}
case IntLess:
return sq.Lt{s.Column: s.Int}
case NumberEquals:
return sq.Eq{s.Column.identifier(): s.Number}, nil
case NumberNotEquals:
return sq.NotEq{s.Column.identifier(): s.Number}, nil
case NumberLess:
return sq.Lt{s.Column.identifier(): s.Number}, nil
case NumberGreater:
return sq.Gt{s.Column.identifier(): s.Number}, nil
case NumberListContains:
return s.Column.identifier() + " @> ? ", []interface{}{pq.Array(s.Number)}
}
return nil
return nil, nil
}
type IntComparison int
type NumberComparison int
const (
IntEquals IntComparison = iota
IntGreater
IntLess
NumberEquals NumberComparison = iota
NumberNotEquals
NumberLess
NumberGreater
NumberListContains
intMax
numberCompareMax
)
//Deprecated: Use NumberComparison, will be removed as soon as all calls are changed to query
func NumberComparisonFromMethod(m domain.SearchMethod) NumberComparison {
switch m {
case domain.SearchMethodEquals:
return NumberEquals
case domain.SearchMethodNotEquals:
return NumberNotEquals
case domain.SearchMethodGreaterThan:
return NumberGreater
case domain.SearchMethodLessThan:
return NumberLess
case domain.SearchMethodListContains:
return NumberListContains
default:
return numberCompareMax
}
}
var (
//countColumn represents the default counter for search responses
countColumn = Column{
name: "COUNT(*) OVER ()",
}
//uniqueColumn shows if there are any results
uniqueColumn = Column{
name: "COUNT(*) = 0",
}
)
type table struct {
name string
alias string
}
func (t table) setAlias(a string) table {
t.alias = a
return t
}
func (t table) identifier() string {
if t.alias == "" {
return t.name
}
return t.name + " as " + t.alias
}
func (t table) isZero() bool {
return t.name == ""
}
type Column struct {
name string
table table
}
func (c Column) identifier() string {
if c.table.alias == "" {
return c.table.name + "." + c.name
}
return c.table.alias + "." + c.name
}
func (c Column) setTable(t table) Column {
c.table = t
return c
}
func (c Column) isZero() bool {
return c.table.isZero() || c.name == ""
}
func join(join, from Column) string {
return join.table.identifier() + " ON " + from.identifier() + " = " + join.identifier()
}

View File

@@ -0,0 +1,767 @@
package query
import (
"errors"
"reflect"
"testing"
sq "github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/domain"
"github.com/lib/pq"
)
var (
testTable = table{
name: "test_table",
alias: "test_table",
}
testCol = Column{
name: "test_col",
table: testTable,
}
testNoCol = Column{
name: "",
table: testTable,
}
)
func TestSearchRequest_ToQuery(t *testing.T) {
type fields struct {
Offset uint64
Limit uint64
SortingColumn Column
Asc bool
}
type want struct {
stmtAddition string
args []interface{}
}
tests := []struct {
name string
fields fields
want want
}{
{
name: "no queries",
fields: fields{},
want: want{
stmtAddition: "",
args: nil,
},
},
{
name: "offset",
fields: fields{
Offset: 5,
},
want: want{
stmtAddition: "OFFSET 5",
args: nil,
},
},
{
name: "limit",
fields: fields{
Limit: 5,
},
want: want{
stmtAddition: "LIMIT 5",
args: nil,
},
},
{
name: "sort asc",
fields: fields{
SortingColumn: testCol,
Asc: true,
},
want: want{
stmtAddition: "ORDER BY LOWER(?)",
args: []interface{}{"test_table.test_col"},
},
},
{
name: "sort desc",
fields: fields{
SortingColumn: testCol,
},
want: want{
stmtAddition: "ORDER BY LOWER(?) DESC",
args: []interface{}{"test_table.test_col"},
},
},
{
name: "all",
fields: fields{
Offset: 5,
Limit: 10,
SortingColumn: testCol,
Asc: true,
},
want: want{
stmtAddition: "ORDER BY LOWER(?) LIMIT 10 OFFSET 5",
args: []interface{}{"test_table.test_col"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := &SearchRequest{
Offset: tt.fields.Offset,
Limit: tt.fields.Limit,
SortingColumn: tt.fields.SortingColumn,
Asc: tt.fields.Asc,
}
query := sq.Select((testCol).identifier()).From(testTable.identifier())
expectedQuery, _, _ := query.ToSql()
stmt, args, err := req.toQuery(query).ToSql()
if len(tt.want.stmtAddition) > 0 {
expectedQuery += " " + tt.want.stmtAddition
}
if expectedQuery != stmt {
t.Errorf("stmt = %q, want %q", stmt, expectedQuery)
}
if !reflect.DeepEqual(args, tt.want.args) {
t.Errorf("args = %v, want %v", args, tt.want.stmtAddition)
}
if err != nil {
t.Errorf("no error expected but got %v", err)
}
})
}
}
func TestNewTextQuery(t *testing.T) {
type args struct {
column Column
value string
compare TextComparison
}
tests := []struct {
name string
args args
want *TextQuery
wantErr func(error) bool
}{
{
name: "too low compare",
args: args{
column: testCol,
value: "hurst",
compare: -1,
},
wantErr: func(err error) bool {
return errors.Is(err, ErrInvalidCompare)
},
},
{
name: "too high compare",
args: args{
column: testCol,
value: "hurst",
compare: textCompareMax,
},
wantErr: func(err error) bool {
return errors.Is(err, ErrInvalidCompare)
},
},
{
name: "no column",
args: args{
column: Column{},
value: "hurst",
compare: TextEquals,
},
wantErr: func(err error) bool {
return errors.Is(err, ErrMissingColumn)
},
},
{
name: "no column name",
args: args{
column: testNoCol,
value: "hurst",
compare: TextEquals,
},
wantErr: func(err error) bool {
return errors.Is(err, ErrMissingColumn)
},
},
{
name: "correct",
args: args{
column: testCol,
value: "hurst",
compare: TextEquals,
},
want: &TextQuery{
Column: testCol,
Text: "hurst",
Compare: TextEquals,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewTextQuery(tt.args.column, tt.args.value, tt.args.compare)
if err != nil && tt.wantErr == nil {
t.Errorf("NewTextQuery() no error expected got %v", err)
return
} else if tt.wantErr != nil && !tt.wantErr(err) {
t.Errorf("NewTextQuery() unexpeted error = %v", err)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewTextQuery() = %v, want %v", got, tt.want)
}
})
}
}
func TestTextQuery_comp(t *testing.T) {
type fields struct {
Column Column
Text string
Compare TextComparison
}
type want struct {
query interface{}
args []interface{}
isNil bool
}
tests := []struct {
name string
fields fields
want want
}{
{
name: "equals",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextEquals,
},
want: want{
query: sq.Eq{"test_table.test_col": "Hurst"},
args: nil,
},
},
{
name: "equals ignore case",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextEqualsIgnoreCase,
},
want: want{
query: sq.ILike{"test_table.test_col": "Hurst"},
args: nil,
},
},
{
name: "starts with",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextStartsWith,
},
want: want{
query: sq.Like{"test_table.test_col": "Hurst%"},
args: nil,
},
},
{
name: "starts with ignore case",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextStartsWithIgnoreCase,
},
want: want{
query: sq.ILike{"test_table.test_col": "Hurst%"},
args: nil,
},
},
{
name: "ends with",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextEndsWith,
},
want: want{
query: sq.Like{"test_table.test_col": "%Hurst"},
args: nil,
},
},
{
name: "ends with ignore case",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextEndsWithIgnoreCase,
},
want: want{
query: sq.ILike{"test_table.test_col": "%Hurst"},
args: nil,
},
},
{
name: "contains",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextContains,
},
want: want{
query: sq.Like{"test_table.test_col": "%Hurst%"},
args: nil,
},
},
{
name: "containts ignore case",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextContainsIgnoreCase,
},
want: want{
query: sq.ILike{"test_table.test_col": "%Hurst%"},
args: nil,
},
},
{
name: "list containts",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextListContains,
},
want: want{
query: "test_table.test_col @> ? ",
args: []interface{}{pq.StringArray{"Hurst"}},
},
},
{
name: "too high comparison",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: textCompareMax,
},
want: want{
isNil: true,
},
},
{
name: "too low comparison",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: -1,
},
want: want{
isNil: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &TextQuery{
Column: tt.fields.Column,
Text: tt.fields.Text,
Compare: tt.fields.Compare,
}
query, args := s.comp()
if query == nil && tt.want.isNil {
return
} else if tt.want.isNil && query != nil {
t.Error("query should not be nil")
}
if !reflect.DeepEqual(query, tt.want.query) {
t.Errorf("wrong query: want: %v, (%T), got: %v, (%T)", tt.want.query, tt.want.query, query, query)
}
if !reflect.DeepEqual(args, tt.want.args) {
t.Errorf("wrong args: want: %v, (%T), got: %v (%T)", tt.want.args, tt.want.args, args, args)
}
})
}
}
func TestTextComparisonFromMethod(t *testing.T) {
type args struct {
m domain.SearchMethod
}
tests := []struct {
name string
args args
want TextComparison
}{
{
name: "equals",
args: args{
m: domain.SearchMethodEquals,
},
want: TextEquals,
},
{
name: "equals ignore case",
args: args{
m: domain.SearchMethodEqualsIgnoreCase,
},
want: TextEqualsIgnoreCase,
},
{
name: "starts with",
args: args{
m: domain.SearchMethodStartsWith,
},
want: TextStartsWith,
},
{
name: "starts with ignore case",
args: args{
m: domain.SearchMethodStartsWithIgnoreCase,
},
want: TextStartsWithIgnoreCase,
},
{
name: "ends with",
args: args{
m: domain.SearchMethodEndsWith,
},
want: TextEndsWith,
},
{
name: "ends with ignore case",
args: args{
m: domain.SearchMethodEndsWithIgnoreCase,
},
want: TextEndsWithIgnoreCase,
},
{
name: "contains",
args: args{
m: domain.SearchMethodContains,
},
want: TextContains,
},
{
name: "list contains",
args: args{
m: domain.SearchMethodListContains,
},
want: TextListContains,
},
{
name: "containts ignore case",
args: args{
m: domain.SearchMethodContainsIgnoreCase,
},
want: TextContainsIgnoreCase,
},
{
name: "invalid search method",
args: args{
m: -1,
},
want: textCompareMax,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := TextComparisonFromMethod(tt.args.m); got != tt.want {
t.Errorf("TextCompareFromMethod() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewNumberQuery(t *testing.T) {
type args struct {
column Column
value interface{}
compare NumberComparison
}
tests := []struct {
name string
args args
want *NumberQuery
wantErr func(error) bool
}{
{
name: "too low compare",
args: args{
column: testCol,
value: "hurst",
compare: -1,
},
wantErr: func(err error) bool {
return errors.Is(err, ErrInvalidCompare)
},
},
{
name: "too high compare",
args: args{
column: testCol,
value: "hurst",
compare: numberCompareMax,
},
wantErr: func(err error) bool {
return errors.Is(err, ErrInvalidCompare)
},
},
{
name: "no column",
args: args{
column: Column{},
value: "hurst",
compare: NumberEquals,
},
wantErr: func(err error) bool {
return errors.Is(err, ErrMissingColumn)
},
},
{
name: "no column name",
args: args{
column: testNoCol,
value: "hurst",
compare: NumberEquals,
},
wantErr: func(err error) bool {
return errors.Is(err, ErrMissingColumn)
},
},
{
name: "no number",
args: args{
column: testCol,
value: "hurst",
compare: NumberEquals,
},
wantErr: func(err error) bool {
return errors.Is(err, ErrInvalidNumber)
},
},
{
name: "correct",
args: args{
column: testCol,
value: 5,
compare: NumberEquals,
},
want: &NumberQuery{
Column: testCol,
Number: 5,
Compare: NumberEquals,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewNumberQuery(tt.args.column, tt.args.value, tt.args.compare)
if err != nil && tt.wantErr == nil {
t.Errorf("NewNumberQuery() no error expected got %v", err)
return
} else if tt.wantErr != nil && !tt.wantErr(err) {
t.Errorf("NewNumberQuery() unexpeted error = %v", err)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewNumberQuery() = %v, want %v", got, tt.want)
}
})
}
}
func TestNumberQuery_comp(t *testing.T) {
type fields struct {
Column Column
Number interface{}
Compare NumberComparison
}
type want struct {
query interface{}
args []interface{}
isNil bool
}
tests := []struct {
name string
fields fields
want want
}{
{
name: "equals",
fields: fields{
Column: testCol,
Number: 42,
Compare: NumberEquals,
},
want: want{
query: sq.Eq{"test_table.test_col": 42},
args: nil,
},
},
{
name: "not equals",
fields: fields{
Column: testCol,
Number: 42,
Compare: NumberNotEquals,
},
want: want{
query: sq.NotEq{"test_table.test_col": 42},
args: nil,
},
},
{
name: "less",
fields: fields{
Column: testCol,
Number: 42,
Compare: NumberLess,
},
want: want{
query: sq.Lt{"test_table.test_col": 42},
args: nil,
},
},
{
name: "greater",
fields: fields{
Column: testCol,
Number: 42,
Compare: NumberGreater,
},
want: want{
query: sq.Gt{"test_table.test_col": 42},
args: nil,
},
},
{
name: "list containts",
fields: fields{
Column: testCol,
Number: 42,
Compare: NumberListContains,
},
want: want{
query: "test_table.test_col @> ? ",
args: []interface{}{pq.Array(42)},
},
},
{
name: "too high comparison",
fields: fields{
Column: testCol,
Number: 42,
Compare: numberCompareMax,
},
want: want{
isNil: true,
},
},
{
name: "too low comparison",
fields: fields{
Column: testCol,
Number: 42,
Compare: -1,
},
want: want{
isNil: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &NumberQuery{
Column: tt.fields.Column,
Number: tt.fields.Number,
Compare: tt.fields.Compare,
}
query, args := s.comp()
if query == nil && tt.want.isNil {
return
} else if tt.want.isNil && query != nil {
t.Error("query should not be nil")
}
if !reflect.DeepEqual(query, tt.want.query) {
t.Errorf("wrong query: want: %v, (%T), got: %v, (%T)", tt.want.query, tt.want.query, query, query)
}
if !reflect.DeepEqual(args, tt.want.args) {
t.Errorf("wrong args: want: %v, (%T), got: %v (%T)", tt.want.args, tt.want.args, args, args)
}
})
}
}
func TestNumberComparisonFromMethod(t *testing.T) {
type args struct {
m domain.SearchMethod
}
tests := []struct {
name string
args args
want NumberComparison
}{
{
name: "equals",
args: args{
m: domain.SearchMethodEquals,
},
want: NumberEquals,
},
{
name: "not equals",
args: args{
m: domain.SearchMethodNotEquals,
},
want: NumberNotEquals,
},
{
name: "less than",
args: args{
m: domain.SearchMethodLessThan,
},
want: NumberLess,
},
{
name: "greater than",
args: args{
m: domain.SearchMethodGreaterThan,
},
want: NumberGreater,
},
{
name: "list contains",
args: args{
m: domain.SearchMethodListContains,
},
want: NumberListContains,
},
{
name: "invalid search method",
args: args{
m: -1,
},
want: numberCompareMax,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NumberComparisonFromMethod(tt.args.m); got != tt.want {
t.Errorf("TextCompareFromMethod() = %v, want %v", got, tt.want)
}
})
}
}