Skip to content

Commit

Permalink
Creates User Lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
gbdubs committed Jan 21, 2024
1 parent 5830d11 commit 27c8be0
Show file tree
Hide file tree
Showing 21 changed files with 678 additions and 18 deletions.
32 changes: 32 additions & 0 deletions cmd/server/pactasrv/conv/oapi_to_pacta.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,35 @@ func AuditLogQueryFromOAPI(q *api.AuditLogQueryReq) (*db.AuditLogQuery, error) {
Sorts: sorts,
}, nil
}

func userQueryWhereFromOAPI(i api.UserQueryWhere) (*db.UserQueryWhere, error) {
result := &db.UserQueryWhere{}
if i.NameOrEmailLike != nil {
result.NameOrEmailLike = *i.NameOrEmailLike
}
return result, nil
}

func UserQueryFromOAPI(q *api.UserQueryReq) (*db.UserQuery, error) {
limit := 100
cursor := ""
if q.Cursor != nil {
cursor = *q.Cursor
}
wheres := []api.UserQueryWhere{}
if q.Wheres != nil {
wheres = append(wheres, *q.Wheres...)
}
ws, err := convAll(wheres, userQueryWhereFromOAPI)
if err != nil {
return nil, oapierr.BadRequest("error converting user query wheres", zap.Error(err))
}
return &db.UserQuery{
Cursor: db.Cursor(cursor),
Limit: limit,
Wheres: ws,
Sorts: []*db.UserQuerySort{
{By: db.UserQuerySortBy_CreatedAt, Ascending: false},
},
}, nil
}
4 changes: 4 additions & 0 deletions cmd/server/pactasrv/conv/pacta_to_oapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ func portfolioInitiativeMembershipToOAPIInitiative(in *pacta.PortfolioInitiative
return out, nil
}

func UsersToOAPI(users []*pacta.User) ([]*api.User, error) {
return convAll(users, UserToOAPI)
}

func UserToOAPI(user *pacta.User) (*api.User, error) {
if user == nil {
return nil, oapierr.Internal("userToOAPI: can't convert nil pointer")
Expand Down
1 change: 1 addition & 0 deletions cmd/server/pactasrv/pactasrv.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ type DB interface {
Users(tx db.Tx, ids []pacta.UserID) (map[pacta.UserID]*pacta.User, error)
UpdateUser(tx db.Tx, id pacta.UserID, mutations ...db.UpdateUserFn) error
DeleteUser(tx db.Tx, id pacta.UserID) ([]pacta.BlobURI, error)
QueryUsers(tx db.Tx, q *db.UserQuery) ([]*pacta.User, *db.PageInfo, error)

CreateAuditLog(tx db.Tx, a *pacta.AuditLog) (pacta.AuditLogID, error)
CreateAuditLogs(tx db.Tx, as []*pacta.AuditLog) error
Expand Down
28 changes: 28 additions & 0 deletions cmd/server/pactasrv/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,34 @@ func (s *Server) UserAuthenticationFollowup(ctx context.Context, _request api.Us
return api.UserAuthenticationFollowup200JSONResponse(*result), nil
}

// (GET /users)
func (s *Server) UserQuery(ctx context.Context, request api.UserQueryRequestObject) (api.UserQueryResponseObject, error) {
actorInfo, err := s.getActorInfoOrErrIfAnon(ctx)
if err != nil {
return nil, err
}
if !actorInfo.IsAdmin && !actorInfo.IsSuperAdmin {
return nil, oapierr.Unauthorized("only admins can list users")
}
q, err := conv.UserQueryFromOAPI(request.Body)
if err != nil {
return nil, err
}
us, pi, err := s.DB.QueryUsers(s.DB.NoTxn(ctx), q)
if err != nil {
return nil, oapierr.Internal("failed to query users", zap.Error(err))
}
users, err := dereference(conv.UsersToOAPI(us))
if err != nil {
return nil, err
}
return api.UserQuery200JSONResponse{
Users: users,
Cursor: string(pi.Cursor),
HasNextPage: pi.HasNextPage,
}, nil
}

func (s *Server) userDoAuthzAndAuditLog(ctx context.Context, targetUserID pacta.UserID, action pacta.AuditLogAction) error {
actorInfo, err := s.getActorInfoOrErrIfAnon(ctx)
if err != nil {
Expand Down
22 changes: 22 additions & 0 deletions db/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,25 @@ type AuditLogQuery struct {
Wheres []*AuditLogQueryWhere
Sorts []*AuditLogQuerySort
}

type UserQuerySortBy string

const (
UserQuerySortBy_CreatedAt UserQuerySortBy = "created_at"
)

type UserQuerySort struct {
By UserQuerySortBy
Ascending bool
}

type UserQueryWhere struct {
NameOrEmailLike string
}

type UserQuery struct {
Cursor Cursor
Limit int
Wheres []*UserQueryWhere
Sorts []*UserQuerySort
}
2 changes: 2 additions & 0 deletions db/sqldb/golden/human_readable_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ ALTER TABLE ONLY pacta_user ADD CONSTRAINT pacta_user_authn_mechanism_authn_id_k
ALTER TABLE ONLY pacta_user ADD CONSTRAINT pacta_user_canonical_email_key UNIQUE (canonical_email);
ALTER TABLE ONLY pacta_user ADD CONSTRAINT pacta_user_entered_email_key UNIQUE (entered_email);
ALTER TABLE ONLY pacta_user ADD CONSTRAINT pacta_user_pkey PRIMARY KEY (id);
CREATE INDEX user_canonical_email_gin_index ON pacta_user USING gin (canonical_email gin_trgm_ops);
CREATE INDEX user_name_gin_index ON pacta_user USING gin (name gin_trgm_ops);


CREATE TABLE pacta_version (
Expand Down
28 changes: 28 additions & 0 deletions db/sqldb/golden/schema_dump.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

--
-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: -
--

CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;


--
-- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner:
--

COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams';


--
-- Name: analysis_type; Type: TYPE; Schema: public; Owner: postgres
--
Expand Down Expand Up @@ -749,6 +763,20 @@ CREATE INDEX owner_by_user_id ON public.owner USING btree (user_id);
CREATE INDEX portfolio_by_blob_id ON public.portfolio USING btree (blob_id);


--
-- Name: user_canonical_email_gin_index; Type: INDEX; Schema: public; Owner: postgres
--

CREATE INDEX user_canonical_email_gin_index ON public.pacta_user USING gin (canonical_email public.gin_trgm_ops);


--
-- Name: user_name_gin_index; Type: INDEX; Schema: public; Owner: postgres
--

CREATE INDEX user_name_gin_index ON public.pacta_user USING gin (name public.gin_trgm_ops);


--
-- Name: schema_migrations track_applied_migrations; Type: TRIGGER; Schema: public; Owner: postgres
--
Expand Down
6 changes: 6 additions & 0 deletions db/sqldb/migrations/0013_user_search.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN;

DROP INDEX user_name_gin_index;
DROP INDEX user_canonical_email_gin_index;

COMMIT;
9 changes: 9 additions & 0 deletions db/sqldb/migrations/0013_user_search.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
BEGIN;

CREATE EXTENSION IF NOT EXISTS pg_trgm;
SET pg_trgm.similarity_threshold = 0.1;

CREATE INDEX user_name_gin_index ON pacta_user USING gin (name gin_trgm_ops);
CREATE INDEX user_canonical_email_gin_index ON pacta_user USING gin (canonical_email gin_trgm_ops);

COMMIT;
1 change: 1 addition & 0 deletions db/sqldb/sqldb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func TestSchemaHistory(t *testing.T) {
{ID: 10, Version: 10}, // 0010_audit_log_enum_values
{ID: 11, Version: 11}, // 0011_add_report_file_types
{ID: 12, Version: 12}, // 0012_portfolio_properties
{ID: 13, Version: 13}, // 0013_user_search
}

if diff := cmp.Diff(want, got); diff != "" {
Expand Down
79 changes: 79 additions & 0 deletions db/sqldb/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sqldb

import (
"fmt"
"strings"

"github.com/RMI/pacta/db"
"github.com/RMI/pacta/pacta"
Expand Down Expand Up @@ -105,6 +106,84 @@ func (d *DB) Users(tx db.Tx, ids []pacta.UserID) (map[pacta.UserID]*pacta.User,
return result, nil
}

func (d *DB) QueryUsers(tx db.Tx, q *db.UserQuery) ([]*pacta.User, *db.PageInfo, error) {
if q.Limit <= 0 {
return nil, nil, fmt.Errorf("limit must be greater than 0, was %d", q.Limit)
}
offset, err := offsetFromCursor(q.Cursor)
if err != nil {
return nil, nil, fmt.Errorf("converting cursor to offset: %w", err)
}
sql, args, err := userQuery(q)
if err != nil {
return nil, nil, fmt.Errorf("building user query: %w", err)
}
rows, err := d.query(tx, sql, args...)
if err != nil {
return nil, nil, fmt.Errorf("executing user query: %w", err)
}
us, err := rowsToUsers(rows)
if err != nil {
return nil, nil, fmt.Errorf("getting users from rows: %w", err)
}
// This will incorrectly say "yes there are more results" if we happen to hit the actual limit, but
// that's a pretty small performance loss.
hasNextPage := len(us) == q.Limit
cursor := offsetToCursor(offset + len(us))
return us, &db.PageInfo{HasNextPage: hasNextPage, Cursor: db.Cursor(cursor)}, nil
}

func userQuery(q *db.UserQuery) (string, []any, error) {
args := &queryArgs{}
selectFrom := `SELECT ` + userSelectColumns + ` FROM pacta_user`
where := userQueryWheresToSQL(q.Wheres, args)
order := userQuerySortsToSQL(q.Sorts)
limit := fmt.Sprintf("LIMIT %d", q.Limit)
offset := ""
if q.Cursor != "" {
o, err := offsetFromCursor(q.Cursor)
if err != nil {
return "", nil, fmt.Errorf("extracting offset from cursor in audit-log query: %w", err)
}
offset = fmt.Sprintf("OFFSET %d", o)
}
sql := fmt.Sprintf("%s %s %s %s %s;", selectFrom, where, order, limit, offset)
return sql, args.values, nil
}

func userQuerySortsToSQL(ss []*db.UserQuerySort) string {
sorts := []string{}
for _, s := range ss {
v := " DESC"
if s.Ascending {
v = " ASC"
}
sorts = append(sorts, fmt.Sprintf("pacta_user.%s %s", s.By, v))
}
// Forces a deterministic sort for pagination.
sorts = append(sorts, "pacta_user.id ASC")
return "ORDER BY " + strings.Join(sorts, ", ")
}

func userQueryWheresToSQL(qs []*db.UserQueryWhere, args *queryArgs) string {
wheres := []string{}
for _, q := range qs {
if q.NameOrEmailLike != "" {
wheres = append(wheres,
fmt.Sprintf(
`name ILIKE ('%[1]s' || %[2]s || '%[1]s')
OR
canonical_email ILIKE ('%[1]s' || %[2]s || '%[1]s')`,
"%",
args.add(q.NameOrEmailLike)))
}
}
if len(wheres) == 0 {
return ""
}
return "WHERE " + strings.Join(wheres, " AND ")
}

func (d *DB) createUser(tx db.Tx, u *pacta.User) (pacta.UserID, error) {
if err := validateUserForCreation(u); err != nil {
return "", fmt.Errorf("validating user for creation: %w", err)
Expand Down
Loading

0 comments on commit 27c8be0

Please sign in to comment.