From 76695955865679335c917899e28efb24b064813e Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 4 Jan 2023 22:41:29 +0100 Subject: [PATCH 1/3] feat!: wip pronoun entry rework --- backend/db/db.go | 13 + backend/db/entries.go | 68 ++ backend/db/field.go | 122 ++-- backend/db/names_pronouns.go | 12 - backend/db/queries/generate.go | 3 + backend/db/queries/queries.member.sql | 31 + backend/db/queries/queries.member.sql.go | 803 +++++++++++++++++++++++ backend/db/queries/queries.user.sql | 21 + backend/db/queries/queries.user.sql.go | 310 +++++++++ backend/db/user.go | 21 +- scripts/migrate/001_init.sql | 2 +- scripts/migrate/004_field_arrays.sql | 35 + 12 files changed, 1348 insertions(+), 93 deletions(-) create mode 100644 backend/db/entries.go create mode 100644 backend/db/queries/generate.go create mode 100644 backend/db/queries/queries.member.sql create mode 100644 backend/db/queries/queries.member.sql.go create mode 100644 backend/db/queries/queries.user.sql create mode 100644 backend/db/queries/queries.user.sql.go create mode 100644 scripts/migrate/004_field_arrays.sql diff --git a/backend/db/db.go b/backend/db/db.go index ed205af..1f16edd 100644 --- a/backend/db/db.go +++ b/backend/db/db.go @@ -7,9 +7,12 @@ import ( "net/url" "os" + "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "codeberg.org/u1f320/pronouns.cc/backend/log" "emperror.dev/errors" "github.com/Masterminds/squirrel" + "github.com/jackc/pgconn" + "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" "github.com/mediocregopher/radix/v4" "github.com/minio/minio-go/v7" @@ -20,6 +23,12 @@ var sq = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) const ErrNothingToUpdate = errors.Sentinel("nothing to update") +type querier interface { + Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) + QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row + Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error) +} + type DB struct { *pgxpool.Pool @@ -28,6 +37,8 @@ type DB struct { minio *minio.Client minioBucket string baseURL *url.URL + + q queries.Querier } func New() (*DB, error) { @@ -67,6 +78,8 @@ func New() (*DB, error) { minio: minioClient, minioBucket: os.Getenv("MINIO_BUCKET"), baseURL: baseURL, + + q: queries.NewQuerier(pool), } return db, nil diff --git a/backend/db/entries.go b/backend/db/entries.go new file mode 100644 index 0000000..02e234a --- /dev/null +++ b/backend/db/entries.go @@ -0,0 +1,68 @@ +package db + +import "codeberg.org/u1f320/pronouns.cc/backend/db/queries" + +type WordStatus int + +const ( + StatusUnknown WordStatus = 0 + StatusFavourite WordStatus = 1 + StatusOkay WordStatus = 2 + StatusJokingly WordStatus = 3 + StatusFriendsOnly WordStatus = 4 + StatusAvoid WordStatus = 5 + wordStatusMax WordStatus = 6 +) + +type FieldEntry struct { + Value string `json:"value"` + Status WordStatus `json:"status"` +} + +type PronounEntry struct { + Pronouns string `json:"pronouns"` + DisplayText *string `json:"display_text"` + Status WordStatus `json:"status"` +} + +func dbEntriesToFieldEntries(entries []queries.FieldEntry) []FieldEntry { + out := make([]FieldEntry, len(entries)) + for i := range entries { + out[i] = FieldEntry{ + *entries[i].Value, WordStatus(*entries[i].Status), + } + } + return out +} + +func dbPronounEntriesToPronounEntries(entries []queries.PronounEntry) []PronounEntry { + out := make([]PronounEntry, len(entries)) + for i := range entries { + out[i] = PronounEntry{ + *entries[i].Value, entries[i].DisplayValue, WordStatus(*entries[i].Status), + } + } + return out +} + +func entriesToDBEntries(entries []FieldEntry) []queries.FieldEntry { + out := make([]queries.FieldEntry, len(entries)) + for i := range entries { + status := int32(entries[i].Status) + out[i] = queries.FieldEntry{ + &entries[i].Value, &status, + } + } + return out +} + +func pronounEntriesToDBEntries(entries []PronounEntry) []queries.PronounEntry { + out := make([]queries.PronounEntry, len(entries)) + for i := range entries { + status := int32(entries[i].Status) + out[i] = queries.PronounEntry{ + &entries[i].Pronouns, entries[i].DisplayText, &status, + } + } + return out +} diff --git a/backend/db/field.go b/backend/db/field.go index d7d69ad..2fa2f34 100644 --- a/backend/db/field.go +++ b/backend/db/field.go @@ -4,8 +4,8 @@ import ( "context" "fmt" + "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "emperror.dev/errors" - "github.com/georgysavva/scany/pgxscan" "github.com/jackc/pgx/v4" "github.com/rs/xid" ) @@ -18,13 +18,9 @@ const ( ) type Field struct { - ID int64 `json:"-"` - Name string `json:"name"` - Favourite []string `json:"favourite"` - Okay []string `json:"okay"` - Jokingly []string `json:"jokingly"` - FriendsOnly []string `json:"friends_only"` - Avoid []string `json:"avoid"` + ID int64 `json:"-"` + Name string `json:"name"` + Entries []FieldEntry `json:"entries"` } // Validate validates this field. If it is invalid, a non-empty string is returned as error message. @@ -37,37 +33,17 @@ func (f Field) Validate() string { return fmt.Sprintf("name max length is %d characters, length is %d", FieldNameMaxLength, length) } - if length := len(f.Favourite) + len(f.Okay) + len(f.Jokingly) + len(f.FriendsOnly) + len(f.Avoid); length > FieldEntriesLimit { + if length := len(f.Entries); length > FieldEntriesLimit { return fmt.Sprintf("max number of entries is %d, current number is %d", FieldEntriesLimit, length) } - for i, entry := range f.Favourite { - if length := len([]rune(entry)); length > FieldEntryMaxLength { - return fmt.Sprintf("favourite.%d: name max length is %d characters, length is %d", i, FieldEntryMaxLength, length) + for i, entry := range f.Entries { + if length := len([]rune(entry.Value)); length > FieldEntryMaxLength { + return fmt.Sprintf("entries.%d: max length is %d characters, length is %d", i, FieldEntryMaxLength, length) } - } - for i, entry := range f.Okay { - if length := len([]rune(entry)); length > FieldEntryMaxLength { - return fmt.Sprintf("okay.%d: name max length is %d characters, length is %d", i, FieldEntryMaxLength, length) - } - } - - for i, entry := range f.Jokingly { - if length := len([]rune(entry)); length > FieldEntryMaxLength { - return fmt.Sprintf("jokingly.%d: name max length is %d characters, length is %d", i, FieldEntryMaxLength, length) - } - } - - for i, entry := range f.FriendsOnly { - if length := len([]rune(entry)); length > FieldEntryMaxLength { - return fmt.Sprintf("friends_only.%d: name max length is %d characters, length is %d", i, FieldEntryMaxLength, length) - } - } - - for i, entry := range f.Avoid { - if length := len([]rune(entry)); length > FieldEntryMaxLength { - return fmt.Sprintf("avoid.%d: name max length is %d characters, length is %d", i, FieldEntryMaxLength, length) + if entry.Status == StatusUnknown || entry.Status >= wordStatusMax { + return fmt.Sprintf("entries.%d: status is invalid, must be between 1 and %d, is %d", i, wordStatusMax-1, entry.Status) } } @@ -76,17 +52,20 @@ func (f Field) Validate() string { // UserFields returns the fields associated with the given user ID. func (db *DB) UserFields(ctx context.Context, id xid.ID) (fs []Field, err error) { - sql, args, err := sq. - Select("id", "name", "favourite", "okay", "jokingly", "friends_only", "avoid"). - From("user_fields").Where("user_id = ?", id).OrderBy("id ASC").ToSql() + qfields, err := db.q.GetUserFields(ctx, id.String()) if err != nil { - return nil, errors.Wrap(err, "building sql") + return nil, errors.Wrap(err, "querying fields") } - err = pgxscan.Select(ctx, db, &fs, sql, args...) - if err != nil { - return nil, errors.Cause(err) + fs = make([]Field, len(qfields)) + for i := range qfields { + fs[i] = Field{ + ID: int64(*qfields[i].ID), + Name: *qfields[i].Name, + Entries: dbEntriesToFieldEntries(qfields[i].Entries), + } } + return fs, nil } @@ -102,20 +81,14 @@ func (db *DB) SetUserFields(ctx context.Context, tx pgx.Tx, userID xid.ID, field return errors.Wrap(err, "deleting existing fields") } - _, err = tx.CopyFrom(ctx, - pgx.Identifier{"user_fields"}, - []string{"user_id", "name", "favourite", "okay", "jokingly", "friends_only", "avoid"}, - pgx.CopyFromSlice(len(fields), func(i int) ([]any, error) { - return []any{ - userID, - fields[i].Name, - fields[i].Favourite, - fields[i].Okay, - fields[i].Jokingly, - fields[i].FriendsOnly, - fields[i].Avoid, - }, nil - })) + querier := queries.NewQuerier(tx) + for _, field := range fields { + querier.InsertUserField(ctx, queries.InsertUserFieldParams{ + UserID: userID.String(), + Name: field.Name, + Entries: entriesToDBEntries(field.Entries), + }) + } if err != nil { return errors.Wrap(err, "inserting new fields") } @@ -124,17 +97,20 @@ func (db *DB) SetUserFields(ctx context.Context, tx pgx.Tx, userID xid.ID, field // MemberFields returns the fields associated with the given member ID. func (db *DB) MemberFields(ctx context.Context, id xid.ID) (fs []Field, err error) { - sql, args, err := sq. - Select("id", "name", "favourite", "okay", "jokingly", "friends_only", "avoid"). - From("member_fields").Where("member_id = ?", id).OrderBy("id ASC").ToSql() + qfields, err := db.q.GetMemberFields(ctx, id.String()) if err != nil { - return nil, errors.Wrap(err, "building sql") + return nil, errors.Wrap(err, "querying fields") } - err = pgxscan.Select(ctx, db, &fs, sql, args...) - if err != nil { - return nil, errors.Cause(err) + fs = make([]Field, len(qfields)) + for i := range qfields { + fs[i] = Field{ + ID: int64(*qfields[i].ID), + Name: *qfields[i].Name, + Entries: dbEntriesToFieldEntries(qfields[i].Entries), + } } + return fs, nil } @@ -150,20 +126,14 @@ func (db *DB) SetMemberFields(ctx context.Context, tx pgx.Tx, memberID xid.ID, f return errors.Wrap(err, "deleting existing fields") } - _, err = tx.CopyFrom(ctx, - pgx.Identifier{"member_fields"}, - []string{"member_id", "name", "favourite", "okay", "jokingly", "friends_only", "avoid"}, - pgx.CopyFromSlice(len(fields), func(i int) ([]any, error) { - return []any{ - memberID, - fields[i].Name, - fields[i].Favourite, - fields[i].Okay, - fields[i].Jokingly, - fields[i].FriendsOnly, - fields[i].Avoid, - }, nil - })) + querier := queries.NewQuerier(tx) + for _, field := range fields { + querier.InsertMemberField(ctx, queries.InsertMemberFieldParams{ + MemberID: memberID.String(), + Name: field.Name, + Entries: entriesToDBEntries(field.Entries), + }) + } if err != nil { return errors.Wrap(err, "inserting new fields") } diff --git a/backend/db/names_pronouns.go b/backend/db/names_pronouns.go index 45fa146..726509a 100644 --- a/backend/db/names_pronouns.go +++ b/backend/db/names_pronouns.go @@ -11,18 +11,6 @@ import ( "github.com/rs/xid" ) -type WordStatus int - -const ( - StatusUnknown WordStatus = 0 - StatusFavourite WordStatus = 1 - StatusOkay WordStatus = 2 - StatusJokingly WordStatus = 3 - StatusFriendsOnly WordStatus = 4 - StatusAvoid WordStatus = 5 - wordStatusMax WordStatus = 6 -) - type Name struct { ID int64 `json:"-"` Name string `json:"name"` diff --git a/backend/db/queries/generate.go b/backend/db/queries/generate.go new file mode 100644 index 0000000..da76eb1 --- /dev/null +++ b/backend/db/queries/generate.go @@ -0,0 +1,3 @@ +package queries + +//go:generate pggen gen go --query-glob queries.user.sql --query-glob queries.member.sql --postgres-connection "postgres://pggen:pggen@localhost/pggen" diff --git a/backend/db/queries/queries.member.sql b/backend/db/queries/queries.member.sql new file mode 100644 index 0000000..c88bf91 --- /dev/null +++ b/backend/db/queries/queries.member.sql @@ -0,0 +1,31 @@ +-- name: GetMemberByID :one +SELECT * FROM members +WHERE id = pggen.arg('id'); + +-- name: GetMemberByName :one +SELECT * FROM members +WHERE user_id = pggen.arg('user_id') AND ( + id = pggen.arg('member_ref') + OR name = pggen.arg('member_ref') +); + +-- name: GetMembers :many +SELECT * FROM members +WHERE user_id = pggen.arg('user_id') +ORDER BY name, id; + +-- name: UpdateMemberNamesPronouns :one +UPDATE members SET +names = pggen.arg('names'), +pronouns = pggen.arg('pronouns') +WHERE id = pggen.arg('id') +RETURNING *; + +-- name: GetMemberFields :many +SELECT * FROM member_fields WHERE member_id = pggen.arg('member_id') ORDER BY id ASC; + +-- name: InsertMemberField :one +INSERT INTO member_fields +(member_id, name, entries) VALUES +(pggen.arg('member_id'), pggen.arg('name'), pggen.arg('entries')) +RETURNING *; diff --git a/backend/db/queries/queries.member.sql.go b/backend/db/queries/queries.member.sql.go new file mode 100644 index 0000000..08a30a0 --- /dev/null +++ b/backend/db/queries/queries.member.sql.go @@ -0,0 +1,803 @@ +// Code generated by pggen. DO NOT EDIT. + +package queries + +import ( + "context" + "fmt" + "github.com/jackc/pgconn" + "github.com/jackc/pgtype" + "github.com/jackc/pgx/v4" +) + +// Querier is a typesafe Go interface backed by SQL queries. +// +// Methods ending with Batch enqueue a query to run later in a pgx.Batch. After +// calling SendBatch on pgx.Conn, pgxpool.Pool, or pgx.Tx, use the Scan methods +// to parse the results. +type Querier interface { + GetMemberByID(ctx context.Context, id string) (GetMemberByIDRow, error) + // GetMemberByIDBatch enqueues a GetMemberByID query into batch to be executed + // later by the batch. + GetMemberByIDBatch(batch genericBatch, id string) + // GetMemberByIDScan scans the result of an executed GetMemberByIDBatch query. + GetMemberByIDScan(results pgx.BatchResults) (GetMemberByIDRow, error) + + GetMemberByName(ctx context.Context, userID string, memberRef string) (GetMemberByNameRow, error) + // GetMemberByNameBatch enqueues a GetMemberByName query into batch to be executed + // later by the batch. + GetMemberByNameBatch(batch genericBatch, userID string, memberRef string) + // GetMemberByNameScan scans the result of an executed GetMemberByNameBatch query. + GetMemberByNameScan(results pgx.BatchResults) (GetMemberByNameRow, error) + + GetMembers(ctx context.Context, userID string) ([]GetMembersRow, error) + // GetMembersBatch enqueues a GetMembers query into batch to be executed + // later by the batch. + GetMembersBatch(batch genericBatch, userID string) + // GetMembersScan scans the result of an executed GetMembersBatch query. + GetMembersScan(results pgx.BatchResults) ([]GetMembersRow, error) + + UpdateMemberNamesPronouns(ctx context.Context, params UpdateMemberNamesPronounsParams) (UpdateMemberNamesPronounsRow, error) + // UpdateMemberNamesPronounsBatch enqueues a UpdateMemberNamesPronouns query into batch to be executed + // later by the batch. + UpdateMemberNamesPronounsBatch(batch genericBatch, params UpdateMemberNamesPronounsParams) + // UpdateMemberNamesPronounsScan scans the result of an executed UpdateMemberNamesPronounsBatch query. + UpdateMemberNamesPronounsScan(results pgx.BatchResults) (UpdateMemberNamesPronounsRow, error) + + GetMemberFields(ctx context.Context, memberID string) ([]GetMemberFieldsRow, error) + // GetMemberFieldsBatch enqueues a GetMemberFields query into batch to be executed + // later by the batch. + GetMemberFieldsBatch(batch genericBatch, memberID string) + // GetMemberFieldsScan scans the result of an executed GetMemberFieldsBatch query. + GetMemberFieldsScan(results pgx.BatchResults) ([]GetMemberFieldsRow, error) + + InsertMemberField(ctx context.Context, params InsertMemberFieldParams) (InsertMemberFieldRow, error) + // InsertMemberFieldBatch enqueues a InsertMemberField query into batch to be executed + // later by the batch. + InsertMemberFieldBatch(batch genericBatch, params InsertMemberFieldParams) + // InsertMemberFieldScan scans the result of an executed InsertMemberFieldBatch query. + InsertMemberFieldScan(results pgx.BatchResults) (InsertMemberFieldRow, error) + + GetUserByID(ctx context.Context, id string) (GetUserByIDRow, error) + // GetUserByIDBatch enqueues a GetUserByID query into batch to be executed + // later by the batch. + GetUserByIDBatch(batch genericBatch, id string) + // GetUserByIDScan scans the result of an executed GetUserByIDBatch query. + GetUserByIDScan(results pgx.BatchResults) (GetUserByIDRow, error) + + GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error) + // GetUserByUsernameBatch enqueues a GetUserByUsername query into batch to be executed + // later by the batch. + GetUserByUsernameBatch(batch genericBatch, username string) + // GetUserByUsernameScan scans the result of an executed GetUserByUsernameBatch query. + GetUserByUsernameScan(results pgx.BatchResults) (GetUserByUsernameRow, error) + + UpdateUserNamesPronouns(ctx context.Context, params UpdateUserNamesPronounsParams) (UpdateUserNamesPronounsRow, error) + // UpdateUserNamesPronounsBatch enqueues a UpdateUserNamesPronouns query into batch to be executed + // later by the batch. + UpdateUserNamesPronounsBatch(batch genericBatch, params UpdateUserNamesPronounsParams) + // UpdateUserNamesPronounsScan scans the result of an executed UpdateUserNamesPronounsBatch query. + UpdateUserNamesPronounsScan(results pgx.BatchResults) (UpdateUserNamesPronounsRow, error) + + GetUserFields(ctx context.Context, userID string) ([]GetUserFieldsRow, error) + // GetUserFieldsBatch enqueues a GetUserFields query into batch to be executed + // later by the batch. + GetUserFieldsBatch(batch genericBatch, userID string) + // GetUserFieldsScan scans the result of an executed GetUserFieldsBatch query. + GetUserFieldsScan(results pgx.BatchResults) ([]GetUserFieldsRow, error) + + InsertUserField(ctx context.Context, params InsertUserFieldParams) (InsertUserFieldRow, error) + // InsertUserFieldBatch enqueues a InsertUserField query into batch to be executed + // later by the batch. + InsertUserFieldBatch(batch genericBatch, params InsertUserFieldParams) + // InsertUserFieldScan scans the result of an executed InsertUserFieldBatch query. + InsertUserFieldScan(results pgx.BatchResults) (InsertUserFieldRow, error) +} + +type DBQuerier struct { + conn genericConn // underlying Postgres transport to use + types *typeResolver // resolve types by name +} + +var _ Querier = &DBQuerier{} + +// genericConn is a connection to a Postgres database. This is usually backed by +// *pgx.Conn, pgx.Tx, or *pgxpool.Pool. +type genericConn interface { + // Query executes sql with args. If there is an error the returned Rows will + // be returned in an error state. So it is allowed to ignore the error + // returned from Query and handle it in Rows. + Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) + + // QueryRow is a convenience wrapper over Query. Any error that occurs while + // querying is deferred until calling Scan on the returned Row. That Row will + // error with pgx.ErrNoRows if no rows are returned. + QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row + + // Exec executes sql. sql can be either a prepared statement name or an SQL + // string. arguments should be referenced positionally from the sql string + // as $1, $2, etc. + Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error) +} + +// genericBatch batches queries to send in a single network request to a +// Postgres server. This is usually backed by *pgx.Batch. +type genericBatch interface { + // Queue queues a query to batch b. query can be an SQL query or the name of a + // prepared statement. See Queue on *pgx.Batch. + Queue(query string, arguments ...interface{}) +} + +// NewQuerier creates a DBQuerier that implements Querier. conn is typically +// *pgx.Conn, pgx.Tx, or *pgxpool.Pool. +func NewQuerier(conn genericConn) *DBQuerier { + return NewQuerierConfig(conn, QuerierConfig{}) +} + +type QuerierConfig struct { + // DataTypes contains pgtype.Value to use for encoding and decoding instead + // of pggen-generated pgtype.ValueTranscoder. + // + // If OIDs are available for an input parameter type and all of its + // transitive dependencies, pggen will use the binary encoding format for + // the input parameter. + DataTypes []pgtype.DataType +} + +// NewQuerierConfig creates a DBQuerier that implements Querier with the given +// config. conn is typically *pgx.Conn, pgx.Tx, or *pgxpool.Pool. +func NewQuerierConfig(conn genericConn, cfg QuerierConfig) *DBQuerier { + return &DBQuerier{conn: conn, types: newTypeResolver(cfg.DataTypes)} +} + +// WithTx creates a new DBQuerier that uses the transaction to run all queries. +func (q *DBQuerier) WithTx(tx pgx.Tx) (*DBQuerier, error) { + return &DBQuerier{conn: tx}, nil +} + +// preparer is any Postgres connection transport that provides a way to prepare +// a statement, most commonly *pgx.Conn. +type preparer interface { + Prepare(ctx context.Context, name, sql string) (sd *pgconn.StatementDescription, err error) +} + +// PrepareAllQueries executes a PREPARE statement for all pggen generated SQL +// queries in querier files. Typical usage is as the AfterConnect callback +// for pgxpool.Config +// +// pgx will use the prepared statement if available. Calling PrepareAllQueries +// is an optional optimization to avoid a network round-trip the first time pgx +// runs a query if pgx statement caching is enabled. +func PrepareAllQueries(ctx context.Context, p preparer) error { + if _, err := p.Prepare(ctx, getMemberByIDSQL, getMemberByIDSQL); err != nil { + return fmt.Errorf("prepare query 'GetMemberByID': %w", err) + } + if _, err := p.Prepare(ctx, getMemberByNameSQL, getMemberByNameSQL); err != nil { + return fmt.Errorf("prepare query 'GetMemberByName': %w", err) + } + if _, err := p.Prepare(ctx, getMembersSQL, getMembersSQL); err != nil { + return fmt.Errorf("prepare query 'GetMembers': %w", err) + } + if _, err := p.Prepare(ctx, updateMemberNamesPronounsSQL, updateMemberNamesPronounsSQL); err != nil { + return fmt.Errorf("prepare query 'UpdateMemberNamesPronouns': %w", err) + } + if _, err := p.Prepare(ctx, getMemberFieldsSQL, getMemberFieldsSQL); err != nil { + return fmt.Errorf("prepare query 'GetMemberFields': %w", err) + } + if _, err := p.Prepare(ctx, insertMemberFieldSQL, insertMemberFieldSQL); err != nil { + return fmt.Errorf("prepare query 'InsertMemberField': %w", err) + } + if _, err := p.Prepare(ctx, getUserByIDSQL, getUserByIDSQL); err != nil { + return fmt.Errorf("prepare query 'GetUserByID': %w", err) + } + if _, err := p.Prepare(ctx, getUserByUsernameSQL, getUserByUsernameSQL); err != nil { + return fmt.Errorf("prepare query 'GetUserByUsername': %w", err) + } + if _, err := p.Prepare(ctx, updateUserNamesPronounsSQL, updateUserNamesPronounsSQL); err != nil { + return fmt.Errorf("prepare query 'UpdateUserNamesPronouns': %w", err) + } + if _, err := p.Prepare(ctx, getUserFieldsSQL, getUserFieldsSQL); err != nil { + return fmt.Errorf("prepare query 'GetUserFields': %w", err) + } + if _, err := p.Prepare(ctx, insertUserFieldSQL, insertUserFieldSQL); err != nil { + return fmt.Errorf("prepare query 'InsertUserField': %w", err) + } + return nil +} + +// FieldEntry represents the Postgres composite type "field_entry". +type FieldEntry struct { + Value *string `json:"value"` + Status *int32 `json:"status"` +} + +// PronounEntry represents the Postgres composite type "pronoun_entry". +type PronounEntry struct { + Value *string `json:"value"` + DisplayValue *string `json:"display_value"` + Status *int32 `json:"status"` +} + +// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. +type typeResolver struct { + connInfo *pgtype.ConnInfo // types by Postgres type name +} + +func newTypeResolver(types []pgtype.DataType) *typeResolver { + ci := pgtype.NewConnInfo() + for _, typ := range types { + if txt, ok := typ.Value.(textPreferrer); ok && typ.OID != unknownOID { + typ.Value = txt.ValueTranscoder + } + ci.RegisterDataType(typ) + } + return &typeResolver{connInfo: ci} +} + +// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. +func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { + typ, ok := tr.connInfo.DataTypeForName(name) + if !ok { + return 0, nil, false + } + v := pgtype.NewValue(typ.Value) + return typ.OID, v.(pgtype.ValueTranscoder), true +} + +// setValue sets the value of a ValueTranscoder to a value that should always +// work and panics if it fails. +func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { + if err := vt.Set(val); err != nil { + panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) + } + return vt +} + +type compositeField struct { + name string // name of the field + typeName string // Postgres type name + defaultVal pgtype.ValueTranscoder // default value to use +} + +func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { + if _, val, ok := tr.findValue(name); ok { + return val + } + fs := make([]pgtype.CompositeTypeField, len(fields)) + vals := make([]pgtype.ValueTranscoder, len(fields)) + isBinaryOk := true + for i, field := range fields { + oid, val, ok := tr.findValue(field.typeName) + if !ok { + oid = unknownOID + val = field.defaultVal + } + isBinaryOk = isBinaryOk && oid != unknownOID + fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} + vals[i] = val + } + // Okay to ignore error because it's only thrown when the number of field + // names does not equal the number of ValueTranscoders. + typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) + if !isBinaryOk { + return textPreferrer{ValueTranscoder: typ, typeName: name} + } + return typ +} + +func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { + if _, val, ok := tr.findValue(name); ok { + return val + } + elemOID, elemVal, ok := tr.findValue(elemName) + elemValFunc := func() pgtype.ValueTranscoder { + return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) + } + if !ok { + elemOID = unknownOID + elemValFunc = defaultVal + } + typ := pgtype.NewArrayType(name, elemOID, elemValFunc) + if elemOID == unknownOID { + return textPreferrer{ValueTranscoder: typ, typeName: name} + } + return typ +} + +// newFieldEntry creates a new pgtype.ValueTranscoder for the Postgres +// composite type 'field_entry'. +func (tr *typeResolver) newFieldEntry() pgtype.ValueTranscoder { + return tr.newCompositeValue( + "field_entry", + compositeField{name: "value", typeName: "text", defaultVal: &pgtype.Text{}}, + compositeField{name: "status", typeName: "int4", defaultVal: &pgtype.Int4{}}, + ) +} + +// newFieldEntryRaw returns all composite fields for the Postgres composite +// type 'field_entry' as a slice of interface{} to encode query parameters. +func (tr *typeResolver) newFieldEntryRaw(v FieldEntry) []interface{} { + return []interface{}{ + v.Value, + v.Status, + } +} + +// newPronounEntry creates a new pgtype.ValueTranscoder for the Postgres +// composite type 'pronoun_entry'. +func (tr *typeResolver) newPronounEntry() pgtype.ValueTranscoder { + return tr.newCompositeValue( + "pronoun_entry", + compositeField{name: "value", typeName: "text", defaultVal: &pgtype.Text{}}, + compositeField{name: "display_value", typeName: "text", defaultVal: &pgtype.Text{}}, + compositeField{name: "status", typeName: "int4", defaultVal: &pgtype.Int4{}}, + ) +} + +// newPronounEntryRaw returns all composite fields for the Postgres composite +// type 'pronoun_entry' as a slice of interface{} to encode query parameters. +func (tr *typeResolver) newPronounEntryRaw(v PronounEntry) []interface{} { + return []interface{}{ + v.Value, + v.DisplayValue, + v.Status, + } +} + +// newFieldEntryArray creates a new pgtype.ValueTranscoder for the Postgres +// '_field_entry' array type. +func (tr *typeResolver) newFieldEntryArray() pgtype.ValueTranscoder { + return tr.newArrayValue("_field_entry", "field_entry", tr.newFieldEntry) +} + +// newFieldEntryArrayInit creates an initialized pgtype.ValueTranscoder for the +// Postgres array type '_field_entry' to encode query parameters. +func (tr *typeResolver) newFieldEntryArrayInit(ps []FieldEntry) pgtype.ValueTranscoder { + dec := tr.newFieldEntryArray() + if err := dec.Set(tr.newFieldEntryArrayRaw(ps)); err != nil { + panic("encode []FieldEntry: " + err.Error()) // should always succeed + } + return textPreferrer{ValueTranscoder: dec, typeName: "_field_entry"} +} + +// newFieldEntryArrayRaw returns all elements for the Postgres array type '_field_entry' +// as a slice of interface{} for use with the pgtype.Value Set method. +func (tr *typeResolver) newFieldEntryArrayRaw(vs []FieldEntry) []interface{} { + elems := make([]interface{}, len(vs)) + for i, v := range vs { + elems[i] = tr.newFieldEntryRaw(v) + } + return elems +} + +// newPronounEntryArray creates a new pgtype.ValueTranscoder for the Postgres +// '_pronoun_entry' array type. +func (tr *typeResolver) newPronounEntryArray() pgtype.ValueTranscoder { + return tr.newArrayValue("_pronoun_entry", "pronoun_entry", tr.newPronounEntry) +} + +// newPronounEntryArrayInit creates an initialized pgtype.ValueTranscoder for the +// Postgres array type '_pronoun_entry' to encode query parameters. +func (tr *typeResolver) newPronounEntryArrayInit(ps []PronounEntry) pgtype.ValueTranscoder { + dec := tr.newPronounEntryArray() + if err := dec.Set(tr.newPronounEntryArrayRaw(ps)); err != nil { + panic("encode []PronounEntry: " + err.Error()) // should always succeed + } + return textPreferrer{ValueTranscoder: dec, typeName: "_pronoun_entry"} +} + +// newPronounEntryArrayRaw returns all elements for the Postgres array type '_pronoun_entry' +// as a slice of interface{} for use with the pgtype.Value Set method. +func (tr *typeResolver) newPronounEntryArrayRaw(vs []PronounEntry) []interface{} { + elems := make([]interface{}, len(vs)) + for i, v := range vs { + elems[i] = tr.newPronounEntryRaw(v) + } + return elems +} + +const getMemberByIDSQL = `SELECT * FROM members +WHERE id = $1;` + +type GetMemberByIDRow struct { + ID string `json:"id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Bio *string `json:"bio"` + AvatarUrls []string `json:"avatar_urls"` + Links []string `json:"links"` + DisplayName *string `json:"display_name"` + Names []FieldEntry `json:"names"` + Pronouns []PronounEntry `json:"pronouns"` +} + +// GetMemberByID implements Querier.GetMemberByID. +func (q *DBQuerier) GetMemberByID(ctx context.Context, id string) (GetMemberByIDRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "GetMemberByID") + row := q.conn.QueryRow(ctx, getMemberByIDSQL, id) + var item GetMemberByIDRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("query GetMemberByID: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign GetMemberByID row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign GetMemberByID row: %w", err) + } + return item, nil +} + +// GetMemberByIDBatch implements Querier.GetMemberByIDBatch. +func (q *DBQuerier) GetMemberByIDBatch(batch genericBatch, id string) { + batch.Queue(getMemberByIDSQL, id) +} + +// GetMemberByIDScan implements Querier.GetMemberByIDScan. +func (q *DBQuerier) GetMemberByIDScan(results pgx.BatchResults) (GetMemberByIDRow, error) { + row := results.QueryRow() + var item GetMemberByIDRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("scan GetMemberByIDBatch row: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign GetMemberByID row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign GetMemberByID row: %w", err) + } + return item, nil +} + +const getMemberByNameSQL = `SELECT * FROM members +WHERE user_id = $1 AND ( + id = $2 + OR name = $2 +);` + +type GetMemberByNameRow struct { + ID string `json:"id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Bio *string `json:"bio"` + AvatarUrls []string `json:"avatar_urls"` + Links []string `json:"links"` + DisplayName *string `json:"display_name"` + Names []FieldEntry `json:"names"` + Pronouns []PronounEntry `json:"pronouns"` +} + +// GetMemberByName implements Querier.GetMemberByName. +func (q *DBQuerier) GetMemberByName(ctx context.Context, userID string, memberRef string) (GetMemberByNameRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "GetMemberByName") + row := q.conn.QueryRow(ctx, getMemberByNameSQL, userID, memberRef) + var item GetMemberByNameRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("query GetMemberByName: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign GetMemberByName row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign GetMemberByName row: %w", err) + } + return item, nil +} + +// GetMemberByNameBatch implements Querier.GetMemberByNameBatch. +func (q *DBQuerier) GetMemberByNameBatch(batch genericBatch, userID string, memberRef string) { + batch.Queue(getMemberByNameSQL, userID, memberRef) +} + +// GetMemberByNameScan implements Querier.GetMemberByNameScan. +func (q *DBQuerier) GetMemberByNameScan(results pgx.BatchResults) (GetMemberByNameRow, error) { + row := results.QueryRow() + var item GetMemberByNameRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("scan GetMemberByNameBatch row: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign GetMemberByName row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign GetMemberByName row: %w", err) + } + return item, nil +} + +const getMembersSQL = `SELECT * FROM members +WHERE user_id = $1 +ORDER BY name, id;` + +type GetMembersRow struct { + ID *string `json:"id"` + UserID *string `json:"user_id"` + Name *string `json:"name"` + Bio *string `json:"bio"` + AvatarUrls []string `json:"avatar_urls"` + Links []string `json:"links"` + DisplayName *string `json:"display_name"` + Names []FieldEntry `json:"names"` + Pronouns []PronounEntry `json:"pronouns"` +} + +// GetMembers implements Querier.GetMembers. +func (q *DBQuerier) GetMembers(ctx context.Context, userID string) ([]GetMembersRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "GetMembers") + rows, err := q.conn.Query(ctx, getMembersSQL, userID) + if err != nil { + return nil, fmt.Errorf("query GetMembers: %w", err) + } + defer rows.Close() + items := []GetMembersRow{} + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + for rows.Next() { + var item GetMembersRow + if err := rows.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { + return nil, fmt.Errorf("scan GetMembers row: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return nil, fmt.Errorf("assign GetMembers row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return nil, fmt.Errorf("assign GetMembers row: %w", err) + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("close GetMembers rows: %w", err) + } + return items, err +} + +// GetMembersBatch implements Querier.GetMembersBatch. +func (q *DBQuerier) GetMembersBatch(batch genericBatch, userID string) { + batch.Queue(getMembersSQL, userID) +} + +// GetMembersScan implements Querier.GetMembersScan. +func (q *DBQuerier) GetMembersScan(results pgx.BatchResults) ([]GetMembersRow, error) { + rows, err := results.Query() + if err != nil { + return nil, fmt.Errorf("query GetMembersBatch: %w", err) + } + defer rows.Close() + items := []GetMembersRow{} + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + for rows.Next() { + var item GetMembersRow + if err := rows.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { + return nil, fmt.Errorf("scan GetMembersBatch row: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return nil, fmt.Errorf("assign GetMembers row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return nil, fmt.Errorf("assign GetMembers row: %w", err) + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("close GetMembersBatch rows: %w", err) + } + return items, err +} + +const updateMemberNamesPronounsSQL = `UPDATE members SET +names = $1, +pronouns = $2 +WHERE id = $3 +RETURNING *;` + +type UpdateMemberNamesPronounsParams struct { + Names []FieldEntry + Pronouns []PronounEntry + ID string +} + +type UpdateMemberNamesPronounsRow struct { + ID string `json:"id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Bio *string `json:"bio"` + AvatarUrls []string `json:"avatar_urls"` + Links []string `json:"links"` + DisplayName *string `json:"display_name"` + Names []FieldEntry `json:"names"` + Pronouns []PronounEntry `json:"pronouns"` +} + +// UpdateMemberNamesPronouns implements Querier.UpdateMemberNamesPronouns. +func (q *DBQuerier) UpdateMemberNamesPronouns(ctx context.Context, params UpdateMemberNamesPronounsParams) (UpdateMemberNamesPronounsRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "UpdateMemberNamesPronouns") + row := q.conn.QueryRow(ctx, updateMemberNamesPronounsSQL, q.types.newFieldEntryArrayInit(params.Names), q.types.newPronounEntryArrayInit(params.Pronouns), params.ID) + var item UpdateMemberNamesPronounsRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("query UpdateMemberNamesPronouns: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign UpdateMemberNamesPronouns row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign UpdateMemberNamesPronouns row: %w", err) + } + return item, nil +} + +// UpdateMemberNamesPronounsBatch implements Querier.UpdateMemberNamesPronounsBatch. +func (q *DBQuerier) UpdateMemberNamesPronounsBatch(batch genericBatch, params UpdateMemberNamesPronounsParams) { + batch.Queue(updateMemberNamesPronounsSQL, q.types.newFieldEntryArrayInit(params.Names), q.types.newPronounEntryArrayInit(params.Pronouns), params.ID) +} + +// UpdateMemberNamesPronounsScan implements Querier.UpdateMemberNamesPronounsScan. +func (q *DBQuerier) UpdateMemberNamesPronounsScan(results pgx.BatchResults) (UpdateMemberNamesPronounsRow, error) { + row := results.QueryRow() + var item UpdateMemberNamesPronounsRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("scan UpdateMemberNamesPronounsBatch row: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign UpdateMemberNamesPronouns row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign UpdateMemberNamesPronouns row: %w", err) + } + return item, nil +} + +const getMemberFieldsSQL = `SELECT * FROM member_fields WHERE member_id = $1 ORDER BY id ASC;` + +type GetMemberFieldsRow struct { + MemberID *string `json:"member_id"` + ID *int `json:"id"` + Name *string `json:"name"` + Entries []FieldEntry `json:"entries"` +} + +// GetMemberFields implements Querier.GetMemberFields. +func (q *DBQuerier) GetMemberFields(ctx context.Context, memberID string) ([]GetMemberFieldsRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "GetMemberFields") + rows, err := q.conn.Query(ctx, getMemberFieldsSQL, memberID) + if err != nil { + return nil, fmt.Errorf("query GetMemberFields: %w", err) + } + defer rows.Close() + items := []GetMemberFieldsRow{} + entriesArray := q.types.newFieldEntryArray() + for rows.Next() { + var item GetMemberFieldsRow + if err := rows.Scan(&item.MemberID, &item.ID, &item.Name, entriesArray); err != nil { + return nil, fmt.Errorf("scan GetMemberFields row: %w", err) + } + if err := entriesArray.AssignTo(&item.Entries); err != nil { + return nil, fmt.Errorf("assign GetMemberFields row: %w", err) + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("close GetMemberFields rows: %w", err) + } + return items, err +} + +// GetMemberFieldsBatch implements Querier.GetMemberFieldsBatch. +func (q *DBQuerier) GetMemberFieldsBatch(batch genericBatch, memberID string) { + batch.Queue(getMemberFieldsSQL, memberID) +} + +// GetMemberFieldsScan implements Querier.GetMemberFieldsScan. +func (q *DBQuerier) GetMemberFieldsScan(results pgx.BatchResults) ([]GetMemberFieldsRow, error) { + rows, err := results.Query() + if err != nil { + return nil, fmt.Errorf("query GetMemberFieldsBatch: %w", err) + } + defer rows.Close() + items := []GetMemberFieldsRow{} + entriesArray := q.types.newFieldEntryArray() + for rows.Next() { + var item GetMemberFieldsRow + if err := rows.Scan(&item.MemberID, &item.ID, &item.Name, entriesArray); err != nil { + return nil, fmt.Errorf("scan GetMemberFieldsBatch row: %w", err) + } + if err := entriesArray.AssignTo(&item.Entries); err != nil { + return nil, fmt.Errorf("assign GetMemberFields row: %w", err) + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("close GetMemberFieldsBatch rows: %w", err) + } + return items, err +} + +const insertMemberFieldSQL = `INSERT INTO member_fields +(member_id, name, entries) VALUES +($1, $2, $3) +RETURNING *;` + +type InsertMemberFieldParams struct { + MemberID string + Name string + Entries []FieldEntry +} + +type InsertMemberFieldRow struct { + MemberID string `json:"member_id"` + ID int `json:"id"` + Name string `json:"name"` + Entries []FieldEntry `json:"entries"` +} + +// InsertMemberField implements Querier.InsertMemberField. +func (q *DBQuerier) InsertMemberField(ctx context.Context, params InsertMemberFieldParams) (InsertMemberFieldRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "InsertMemberField") + row := q.conn.QueryRow(ctx, insertMemberFieldSQL, params.MemberID, params.Name, q.types.newFieldEntryArrayInit(params.Entries)) + var item InsertMemberFieldRow + entriesArray := q.types.newFieldEntryArray() + if err := row.Scan(&item.MemberID, &item.ID, &item.Name, entriesArray); err != nil { + return item, fmt.Errorf("query InsertMemberField: %w", err) + } + if err := entriesArray.AssignTo(&item.Entries); err != nil { + return item, fmt.Errorf("assign InsertMemberField row: %w", err) + } + return item, nil +} + +// InsertMemberFieldBatch implements Querier.InsertMemberFieldBatch. +func (q *DBQuerier) InsertMemberFieldBatch(batch genericBatch, params InsertMemberFieldParams) { + batch.Queue(insertMemberFieldSQL, params.MemberID, params.Name, q.types.newFieldEntryArrayInit(params.Entries)) +} + +// InsertMemberFieldScan implements Querier.InsertMemberFieldScan. +func (q *DBQuerier) InsertMemberFieldScan(results pgx.BatchResults) (InsertMemberFieldRow, error) { + row := results.QueryRow() + var item InsertMemberFieldRow + entriesArray := q.types.newFieldEntryArray() + if err := row.Scan(&item.MemberID, &item.ID, &item.Name, entriesArray); err != nil { + return item, fmt.Errorf("scan InsertMemberFieldBatch row: %w", err) + } + if err := entriesArray.AssignTo(&item.Entries); err != nil { + return item, fmt.Errorf("assign InsertMemberField row: %w", err) + } + return item, nil +} + +// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding +// format to text instead binary (the default). pggen uses the text format +// when the OID is unknownOID because the binary format requires the OID. +// Typically occurs if the results from QueryAllDataTypes aren't passed to +// NewQuerierConfig. +type textPreferrer struct { + pgtype.ValueTranscoder + typeName string +} + +// PreferredParamFormat implements pgtype.ParamFormatPreferrer. +func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } + +func (t textPreferrer) NewTypeValue() pgtype.Value { + return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} +} + +func (t textPreferrer) TypeName() string { + return t.typeName +} + +// unknownOID means we don't know the OID for a type. This is okay for decoding +// because pgx call DecodeText or DecodeBinary without requiring the OID. For +// encoding parameters, pggen uses textPreferrer if the OID is unknown. +const unknownOID = 0 diff --git a/backend/db/queries/queries.user.sql b/backend/db/queries/queries.user.sql new file mode 100644 index 0000000..add0b9c --- /dev/null +++ b/backend/db/queries/queries.user.sql @@ -0,0 +1,21 @@ +-- name: GetUserByID :one +SELECT * FROM users WHERE id = pggen.arg('id'); + +-- name: GetUserByUsername :one +SELECT * FROM users WHERE username = pggen.arg('username'); + +-- name: UpdateUserNamesPronouns :one +UPDATE users SET +names = pggen.arg('names'), +pronouns = pggen.arg('pronouns') +WHERE id = pggen.arg('id') +RETURNING *; + +-- name: GetUserFields :many +SELECT * FROM user_fields WHERE user_id = pggen.arg('user_id') ORDER BY id ASC; + +-- name: InsertUserField :one +INSERT INTO user_fields +(user_id, name, entries) VALUES +(pggen.arg('user_id'), pggen.arg('name'), pggen.arg('entries')) +RETURNING *; diff --git a/backend/db/queries/queries.user.sql.go b/backend/db/queries/queries.user.sql.go new file mode 100644 index 0000000..5276e83 --- /dev/null +++ b/backend/db/queries/queries.user.sql.go @@ -0,0 +1,310 @@ +// Code generated by pggen. DO NOT EDIT. + +package queries + +import ( + "context" + "fmt" + "github.com/jackc/pgx/v4" +) + +const getUserByIDSQL = `SELECT * FROM users WHERE id = $1;` + +type GetUserByIDRow struct { + ID string `json:"id"` + Username string `json:"username"` + DisplayName *string `json:"display_name"` + Bio *string `json:"bio"` + AvatarUrls []string `json:"avatar_urls"` + Links []string `json:"links"` + Discord *string `json:"discord"` + DiscordUsername *string `json:"discord_username"` + MaxInvites int32 `json:"max_invites"` + Names []FieldEntry `json:"names"` + Pronouns []PronounEntry `json:"pronouns"` +} + +// GetUserByID implements Querier.GetUserByID. +func (q *DBQuerier) GetUserByID(ctx context.Context, id string) (GetUserByIDRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "GetUserByID") + row := q.conn.QueryRow(ctx, getUserByIDSQL, id) + var item GetUserByIDRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("query GetUserByID: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign GetUserByID row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign GetUserByID row: %w", err) + } + return item, nil +} + +// GetUserByIDBatch implements Querier.GetUserByIDBatch. +func (q *DBQuerier) GetUserByIDBatch(batch genericBatch, id string) { + batch.Queue(getUserByIDSQL, id) +} + +// GetUserByIDScan implements Querier.GetUserByIDScan. +func (q *DBQuerier) GetUserByIDScan(results pgx.BatchResults) (GetUserByIDRow, error) { + row := results.QueryRow() + var item GetUserByIDRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("scan GetUserByIDBatch row: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign GetUserByID row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign GetUserByID row: %w", err) + } + return item, nil +} + +const getUserByUsernameSQL = `SELECT * FROM users WHERE username = $1;` + +type GetUserByUsernameRow struct { + ID string `json:"id"` + Username string `json:"username"` + DisplayName *string `json:"display_name"` + Bio *string `json:"bio"` + AvatarUrls []string `json:"avatar_urls"` + Links []string `json:"links"` + Discord *string `json:"discord"` + DiscordUsername *string `json:"discord_username"` + MaxInvites int32 `json:"max_invites"` + Names []FieldEntry `json:"names"` + Pronouns []PronounEntry `json:"pronouns"` +} + +// GetUserByUsername implements Querier.GetUserByUsername. +func (q *DBQuerier) GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "GetUserByUsername") + row := q.conn.QueryRow(ctx, getUserByUsernameSQL, username) + var item GetUserByUsernameRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("query GetUserByUsername: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign GetUserByUsername row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign GetUserByUsername row: %w", err) + } + return item, nil +} + +// GetUserByUsernameBatch implements Querier.GetUserByUsernameBatch. +func (q *DBQuerier) GetUserByUsernameBatch(batch genericBatch, username string) { + batch.Queue(getUserByUsernameSQL, username) +} + +// GetUserByUsernameScan implements Querier.GetUserByUsernameScan. +func (q *DBQuerier) GetUserByUsernameScan(results pgx.BatchResults) (GetUserByUsernameRow, error) { + row := results.QueryRow() + var item GetUserByUsernameRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("scan GetUserByUsernameBatch row: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign GetUserByUsername row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign GetUserByUsername row: %w", err) + } + return item, nil +} + +const updateUserNamesPronounsSQL = `UPDATE users SET +names = $1, +pronouns = $2 +WHERE id = $3 +RETURNING *;` + +type UpdateUserNamesPronounsParams struct { + Names []FieldEntry + Pronouns []PronounEntry + ID string +} + +type UpdateUserNamesPronounsRow struct { + ID string `json:"id"` + Username string `json:"username"` + DisplayName *string `json:"display_name"` + Bio *string `json:"bio"` + AvatarUrls []string `json:"avatar_urls"` + Links []string `json:"links"` + Discord *string `json:"discord"` + DiscordUsername *string `json:"discord_username"` + MaxInvites int32 `json:"max_invites"` + Names []FieldEntry `json:"names"` + Pronouns []PronounEntry `json:"pronouns"` +} + +// UpdateUserNamesPronouns implements Querier.UpdateUserNamesPronouns. +func (q *DBQuerier) UpdateUserNamesPronouns(ctx context.Context, params UpdateUserNamesPronounsParams) (UpdateUserNamesPronounsRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "UpdateUserNamesPronouns") + row := q.conn.QueryRow(ctx, updateUserNamesPronounsSQL, q.types.newFieldEntryArrayInit(params.Names), q.types.newPronounEntryArrayInit(params.Pronouns), params.ID) + var item UpdateUserNamesPronounsRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("query UpdateUserNamesPronouns: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign UpdateUserNamesPronouns row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign UpdateUserNamesPronouns row: %w", err) + } + return item, nil +} + +// UpdateUserNamesPronounsBatch implements Querier.UpdateUserNamesPronounsBatch. +func (q *DBQuerier) UpdateUserNamesPronounsBatch(batch genericBatch, params UpdateUserNamesPronounsParams) { + batch.Queue(updateUserNamesPronounsSQL, q.types.newFieldEntryArrayInit(params.Names), q.types.newPronounEntryArrayInit(params.Pronouns), params.ID) +} + +// UpdateUserNamesPronounsScan implements Querier.UpdateUserNamesPronounsScan. +func (q *DBQuerier) UpdateUserNamesPronounsScan(results pgx.BatchResults) (UpdateUserNamesPronounsRow, error) { + row := results.QueryRow() + var item UpdateUserNamesPronounsRow + namesArray := q.types.newFieldEntryArray() + pronounsArray := q.types.newPronounEntryArray() + if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil { + return item, fmt.Errorf("scan UpdateUserNamesPronounsBatch row: %w", err) + } + if err := namesArray.AssignTo(&item.Names); err != nil { + return item, fmt.Errorf("assign UpdateUserNamesPronouns row: %w", err) + } + if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { + return item, fmt.Errorf("assign UpdateUserNamesPronouns row: %w", err) + } + return item, nil +} + +const getUserFieldsSQL = `SELECT * FROM user_fields WHERE user_id = $1 ORDER BY id ASC;` + +type GetUserFieldsRow struct { + UserID *string `json:"user_id"` + ID *int `json:"id"` + Name *string `json:"name"` + Entries []FieldEntry `json:"entries"` +} + +// GetUserFields implements Querier.GetUserFields. +func (q *DBQuerier) GetUserFields(ctx context.Context, userID string) ([]GetUserFieldsRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "GetUserFields") + rows, err := q.conn.Query(ctx, getUserFieldsSQL, userID) + if err != nil { + return nil, fmt.Errorf("query GetUserFields: %w", err) + } + defer rows.Close() + items := []GetUserFieldsRow{} + entriesArray := q.types.newFieldEntryArray() + for rows.Next() { + var item GetUserFieldsRow + if err := rows.Scan(&item.UserID, &item.ID, &item.Name, entriesArray); err != nil { + return nil, fmt.Errorf("scan GetUserFields row: %w", err) + } + if err := entriesArray.AssignTo(&item.Entries); err != nil { + return nil, fmt.Errorf("assign GetUserFields row: %w", err) + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("close GetUserFields rows: %w", err) + } + return items, err +} + +// GetUserFieldsBatch implements Querier.GetUserFieldsBatch. +func (q *DBQuerier) GetUserFieldsBatch(batch genericBatch, userID string) { + batch.Queue(getUserFieldsSQL, userID) +} + +// GetUserFieldsScan implements Querier.GetUserFieldsScan. +func (q *DBQuerier) GetUserFieldsScan(results pgx.BatchResults) ([]GetUserFieldsRow, error) { + rows, err := results.Query() + if err != nil { + return nil, fmt.Errorf("query GetUserFieldsBatch: %w", err) + } + defer rows.Close() + items := []GetUserFieldsRow{} + entriesArray := q.types.newFieldEntryArray() + for rows.Next() { + var item GetUserFieldsRow + if err := rows.Scan(&item.UserID, &item.ID, &item.Name, entriesArray); err != nil { + return nil, fmt.Errorf("scan GetUserFieldsBatch row: %w", err) + } + if err := entriesArray.AssignTo(&item.Entries); err != nil { + return nil, fmt.Errorf("assign GetUserFields row: %w", err) + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("close GetUserFieldsBatch rows: %w", err) + } + return items, err +} + +const insertUserFieldSQL = `INSERT INTO user_fields +(user_id, name, entries) VALUES +($1, $2, $3) +RETURNING *;` + +type InsertUserFieldParams struct { + UserID string + Name string + Entries []FieldEntry +} + +type InsertUserFieldRow struct { + UserID string `json:"user_id"` + ID int `json:"id"` + Name string `json:"name"` + Entries []FieldEntry `json:"entries"` +} + +// InsertUserField implements Querier.InsertUserField. +func (q *DBQuerier) InsertUserField(ctx context.Context, params InsertUserFieldParams) (InsertUserFieldRow, error) { + ctx = context.WithValue(ctx, "pggen_query_name", "InsertUserField") + row := q.conn.QueryRow(ctx, insertUserFieldSQL, params.UserID, params.Name, q.types.newFieldEntryArrayInit(params.Entries)) + var item InsertUserFieldRow + entriesArray := q.types.newFieldEntryArray() + if err := row.Scan(&item.UserID, &item.ID, &item.Name, entriesArray); err != nil { + return item, fmt.Errorf("query InsertUserField: %w", err) + } + if err := entriesArray.AssignTo(&item.Entries); err != nil { + return item, fmt.Errorf("assign InsertUserField row: %w", err) + } + return item, nil +} + +// InsertUserFieldBatch implements Querier.InsertUserFieldBatch. +func (q *DBQuerier) InsertUserFieldBatch(batch genericBatch, params InsertUserFieldParams) { + batch.Queue(insertUserFieldSQL, params.UserID, params.Name, q.types.newFieldEntryArrayInit(params.Entries)) +} + +// InsertUserFieldScan implements Querier.InsertUserFieldScan. +func (q *DBQuerier) InsertUserFieldScan(results pgx.BatchResults) (InsertUserFieldRow, error) { + row := results.QueryRow() + var item InsertUserFieldRow + entriesArray := q.types.newFieldEntryArray() + if err := row.Scan(&item.UserID, &item.ID, &item.Name, entriesArray); err != nil { + return item, fmt.Errorf("scan InsertUserFieldBatch row: %w", err) + } + if err := entriesArray.AssignTo(&item.Entries); err != nil { + return item, fmt.Errorf("assign InsertUserField row: %w", err) + } + return item, nil +} diff --git a/backend/db/user.go b/backend/db/user.go index c4ce209..9c13205 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -4,6 +4,7 @@ import ( "context" "regexp" + "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "emperror.dev/errors" "github.com/bwmarrin/discordgo" "github.com/georgysavva/scany/pgxscan" @@ -98,7 +99,7 @@ func (db *DB) DiscordUser(ctx context.Context, discordID string) (u User, err er return u, nil } -func (u *User) UpdateFromDiscord(ctx context.Context, db pgxscan.Querier, du *discordgo.User) error { +func (u *User) UpdateFromDiscord(ctx context.Context, db querier, du *discordgo.User) error { builder := sq.Update("users"). Set("discord", du.ID). Set("discord_username", du.String()). @@ -113,14 +114,26 @@ func (u *User) UpdateFromDiscord(ctx context.Context, db pgxscan.Querier, du *di return pgxscan.Get(ctx, db, u, sql, args...) } -func (db *DB) getUser(ctx context.Context, q pgxscan.Querier, id xid.ID) (u User, err error) { - err = pgxscan.Get(ctx, q, &u, "select * from users where id = $1", id) +func (db *DB) getUser(ctx context.Context, q querier, id xid.ID) (u User, err error) { + qu, err := queries.NewQuerier(q).GetUserByID(ctx, id.String()) if err != nil { if errors.Cause(err) == pgx.ErrNoRows { return u, ErrUserNotFound } - return u, errors.Cause(err) + return u, errors.Wrap(err, "getting user from database") + } + + u = User{ + ID: id, + Username: qu.Username, + DisplayName: qu.DisplayName, + Bio: qu.Bio, + AvatarURLs: qu.AvatarUrls, + Links: qu.Links, + Discord: qu.Discord, + DiscordUsername: qu.DiscordUsername, + MaxInvites: int(qu.MaxInvites), } return u, nil diff --git a/scripts/migrate/001_init.sql b/scripts/migrate/001_init.sql index a60e2aa..2e83fb1 100644 --- a/scripts/migrate/001_init.sql +++ b/scripts/migrate/001_init.sql @@ -14,7 +14,7 @@ create table users ( discord text unique, -- for Discord oauth discord_username text, - max_invites int default 10 + max_invites int not null default 10 ); create table user_names ( diff --git a/scripts/migrate/004_field_arrays.sql b/scripts/migrate/004_field_arrays.sql new file mode 100644 index 0000000..0606cb4 --- /dev/null +++ b/scripts/migrate/004_field_arrays.sql @@ -0,0 +1,35 @@ +-- +migrate Up + +-- 2023-01-03: change names, pronouns, and fields to be columns instead of separate tables + +create type field_entry as ( + value text, + status int +); + +create type pronoun_entry as ( + value text, + display_value text, + status int +); + +alter table users add column names field_entry[]; +alter table users add column pronouns pronoun_entry[]; + +alter table members add column names field_entry[]; +alter table members add column pronouns pronoun_entry[]; + +alter table user_fields add column entries field_entry[]; +alter table member_fields add column entries field_entry[]; + +alter table user_fields drop column favourite; +alter table user_fields drop column okay; +alter table user_fields drop column jokingly; +alter table user_fields drop column friends_only; +alter table user_fields drop column avoid; + +alter table member_fields drop column favourite; +alter table member_fields drop column okay; +alter table member_fields drop column jokingly; +alter table member_fields drop column friends_only; +alter table member_fields drop column avoid; From c6537c920d47a9574cddd126e4fe688032babf18 Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 14 Jan 2023 17:33:18 +0100 Subject: [PATCH 2/3] feat: read/write improved fields for users, read improved names/pronouns for users --- Makefile | 4 +++ backend/db/names_pronouns.go | 53 ++++++++++++++----------------- backend/db/user.go | 28 ++++++++++++++-- backend/routes/bot/bot.go | 16 +++++++--- backend/routes/user/get_user.go | 48 ++++------------------------ backend/routes/user/patch_user.go | 24 +++----------- frontend/lib/api-fetch.ts | 11 ++++--- frontend/lib/api.ts | 30 +++-------------- 8 files changed, 87 insertions(+), 127 deletions(-) diff --git a/Makefile b/Makefile index 2e52632..25af4c1 100644 --- a/Makefile +++ b/Makefile @@ -9,3 +9,7 @@ seeddb: .PHONY: backend backend: CGO_ENABLED=0 go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/u1f320/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD`" ./backend + +.PHONY: generate +generate: + go generate ./... diff --git a/backend/db/names_pronouns.go b/backend/db/names_pronouns.go index 726509a..e5d0907 100644 --- a/backend/db/names_pronouns.go +++ b/backend/db/names_pronouns.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "emperror.dev/errors" "github.com/georgysavva/scany/pgxscan" "github.com/jackc/pgx/v4" @@ -75,35 +76,6 @@ func (p Pronoun) String() string { return strings.Join(split[:1], "/") } -func (db *DB) UserNames(ctx context.Context, userID xid.ID) (ns []Name, err error) { - sql, args, err := sq.Select("id", "name", "status").From("user_names").Where("user_id = ?", userID).OrderBy("id").ToSql() - if err != nil { - return nil, errors.Wrap(err, "building sql") - } - - err = pgxscan.Select(ctx, db, &ns, sql, args...) - if err != nil { - return nil, errors.Wrap(err, "executing query") - } - return ns, nil -} - -func (db *DB) UserPronouns(ctx context.Context, userID xid.ID) (ps []Pronoun, err error) { - sql, args, err := sq. - Select("id", "display_text", "pronouns", "status"). - From("user_pronouns").Where("user_id = ?", userID). - OrderBy("id").ToSql() - if err != nil { - return nil, errors.Wrap(err, "building sql") - } - - err = pgxscan.Select(ctx, db, &ps, sql, args...) - if err != nil { - return nil, errors.Wrap(err, "executing query") - } - return ps, nil -} - func (db *DB) SetUserNames(ctx context.Context, tx pgx.Tx, userID xid.ID, names []Name) (err error) { sql, args, err := sq.Delete("user_names").Where("user_id = ?", userID).ToSql() if err != nil { @@ -242,3 +214,26 @@ func (db *DB) SetMemberPronouns(ctx context.Context, tx pgx.Tx, memberID xid.ID, } return nil } + +func namesFromDB(dn []queries.FieldEntry) []Name { + names := make([]Name, len(dn)) + for i := range dn { + names[i] = Name{ + Name: *dn[i].Value, + Status: WordStatus(*dn[i].Status), + } + } + return names +} + +func pronounsFromDB(dn []queries.PronounEntry) []Pronoun { + pronouns := make([]Pronoun, len(dn)) + for i := range dn { + pronouns[i] = Pronoun{ + DisplayText: dn[i].DisplayValue, + Pronouns: *dn[i].Value, + Status: WordStatus(*dn[i].Status), + } + } + return pronouns +} diff --git a/backend/db/user.go b/backend/db/user.go index 9c13205..4b68afe 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -22,6 +22,9 @@ type User struct { AvatarURLs []string `db:"avatar_urls"` Links []string + Names []Name + Pronouns []Pronoun + Discord *string DiscordUsername *string @@ -130,6 +133,8 @@ func (db *DB) getUser(ctx context.Context, q querier, id xid.ID) (u User, err er DisplayName: qu.DisplayName, Bio: qu.Bio, AvatarURLs: qu.AvatarUrls, + Names: namesFromDB(qu.Names), + Pronouns: pronounsFromDB(qu.Pronouns), Links: qu.Links, Discord: qu.Discord, DiscordUsername: qu.DiscordUsername, @@ -146,13 +151,32 @@ func (db *DB) User(ctx context.Context, id xid.ID) (u User, err error) { // Username gets a user by username. func (db *DB) Username(ctx context.Context, name string) (u User, err error) { - err = pgxscan.Get(ctx, db, &u, "select * from users where username = $1", name) + qu, err := db.q.GetUserByUsername(ctx, name) if err != nil { if errors.Cause(err) == pgx.ErrNoRows { return u, ErrUserNotFound } - return u, errors.Cause(err) + return u, errors.Wrap(err, "getting user from db") + } + + id, err := xid.FromString(qu.ID) + if err != nil { + return u, errors.Wrap(err, "parsing ID") + } + + u = User{ + ID: id, + Username: qu.Username, + DisplayName: qu.DisplayName, + Bio: qu.Bio, + AvatarURLs: qu.AvatarUrls, + Names: namesFromDB(qu.Names), + Pronouns: pronounsFromDB(qu.Pronouns), + Links: qu.Links, + Discord: qu.Discord, + DiscordUsername: qu.DiscordUsername, + MaxInvites: int(qu.MaxInvites), } return u, nil diff --git a/backend/routes/bot/bot.go b/backend/routes/bot/bot.go index 251d027..5c1aa8f 100644 --- a/backend/routes/bot/bot.go +++ b/backend/routes/bot/bot.go @@ -133,17 +133,25 @@ func (bot *Bot) userPronouns(w http.ResponseWriter, r *http.Request, ev *discord } for _, field := range fields { - if len(field.Favourite) == 0 { + var favs []db.FieldEntry + + for _, e := range field.Entries { + if e.Status == db.StatusFavourite { + favs = append(favs, e) + } + } + + if len(favs) == 0 { continue } var value string - for _, fav := range field.Favourite { - if len(value) > 500 { + for _, fav := range favs { + if len(fav.Value) > 500 { break } - value += fav + "\n" + value += fav.Value + "\n" } e.Fields = append(e.Fields, &discordgo.MessageEmbedField{ diff --git a/backend/routes/user/get_user.go b/backend/routes/user/get_user.go index dcb63a7..78b243c 100644 --- a/backend/routes/user/get_user.go +++ b/backend/routes/user/get_user.go @@ -38,7 +38,7 @@ type PartialMember struct { AvatarURLs []string `json:"avatar_urls"` } -func dbUserToResponse(u db.User, fields []db.Field, names []db.Name, pronouns []db.Pronoun, members []db.Member) GetUserResponse { +func dbUserToResponse(u db.User, fields []db.Field, members []db.Member) GetUserResponse { resp := GetUserResponse{ ID: u.ID, Username: u.Username, @@ -46,8 +46,8 @@ func dbUserToResponse(u db.User, fields []db.Field, names []db.Name, pronouns [] Bio: u.Bio, AvatarURLs: u.AvatarURLs, Links: u.Links, - Names: names, - Pronouns: pronouns, + Names: u.Names, + Pronouns: u.Pronouns, Fields: fields, } @@ -78,25 +78,13 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) error { return err } - names, err := s.DB.UserNames(ctx, u.ID) - if err != nil { - log.Errorf("getting user names: %v", err) - return err - } - - pronouns, err := s.DB.UserPronouns(ctx, u.ID) - if err != nil { - log.Errorf("getting user pronouns: %v", err) - return err - } - members, err := s.DB.UserMembers(ctx, u.ID) if err != nil { log.Errorf("Error getting user members: %v", err) return err } - render.JSON(w, r, dbUserToResponse(u, fields, names, pronouns, members)) + render.JSON(w, r, dbUserToResponse(u, fields, members)) return nil } else if err != db.ErrUserNotFound { log.Errorf("Error getting user by ID: %v", err) @@ -116,18 +104,6 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) error { return err } - names, err := s.DB.UserNames(ctx, u.ID) - if err != nil { - log.Errorf("getting user names: %v", err) - return err - } - - pronouns, err := s.DB.UserPronouns(ctx, u.ID) - if err != nil { - log.Errorf("getting user pronouns: %v", err) - return err - } - fields, err := s.DB.UserFields(ctx, u.ID) if err != nil { log.Errorf("Error getting user fields: %v", err) @@ -140,7 +116,7 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) error { return err } - render.JSON(w, r, dbUserToResponse(u, fields, names, pronouns, members)) + render.JSON(w, r, dbUserToResponse(u, fields, members)) return nil } @@ -154,18 +130,6 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error { return err } - names, err := s.DB.UserNames(ctx, u.ID) - if err != nil { - log.Errorf("getting user names: %v", err) - return err - } - - pronouns, err := s.DB.UserPronouns(ctx, u.ID) - if err != nil { - log.Errorf("getting user pronouns: %v", err) - return err - } - fields, err := s.DB.UserFields(ctx, u.ID) if err != nil { log.Errorf("Error getting user fields: %v", err) @@ -179,7 +143,7 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error { } render.JSON(w, r, GetMeResponse{ - GetUserResponse: dbUserToResponse(u, fields, names, pronouns, members), + GetUserResponse: dbUserToResponse(u, fields, members), Discord: u.Discord, DiscordUsername: u.DiscordUsername, }) diff --git a/backend/routes/user/patch_user.go b/backend/routes/user/patch_user.go index 3e8561b..b46e9f7 100644 --- a/backend/routes/user/patch_user.go +++ b/backend/routes/user/patch_user.go @@ -159,11 +159,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { return err } - var ( - names []db.Name - pronouns []db.Pronoun - fields []db.Field - ) + var fields []db.Field if req.Names != nil { err = s.DB.SetUserNames(ctx, tx, claims.UserID, *req.Names) @@ -171,13 +167,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { log.Errorf("setting names for user %v: %v", claims.UserID, err) return err } - names = *req.Names - } else { - names, err = s.DB.UserNames(ctx, claims.UserID) - if err != nil { - log.Errorf("getting names for user %v: %v", claims.UserID, err) - return err - } + u.Names = *req.Names } if req.Pronouns != nil { @@ -186,13 +176,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { log.Errorf("setting pronouns for user %v: %v", claims.UserID, err) return err } - pronouns = *req.Pronouns - } else { - pronouns, err = s.DB.UserPronouns(ctx, claims.UserID) - if err != nil { - log.Errorf("getting fields for user %v: %v", claims.UserID, err) - return err - } + u.Pronouns = *req.Pronouns } if req.Fields != nil { @@ -217,7 +201,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { } // echo the updated user back on success - render.JSON(w, r, dbUserToResponse(u, fields, names, pronouns, nil)) + render.JSON(w, r, dbUserToResponse(u, fields, nil)) return nil } diff --git a/frontend/lib/api-fetch.ts b/frontend/lib/api-fetch.ts index a8eedf6..1d8f590 100644 --- a/frontend/lib/api-fetch.ts +++ b/frontend/lib/api-fetch.ts @@ -51,11 +51,12 @@ export interface Pronoun { export interface Field { name: string; - favourite: Arr; - okay: Arr; - jokingly: Arr; - friends_only: Arr; - avoid: Arr; + entries: Arr; +} + +export interface FieldEntry { + value: string; + status: WordStatus; } export interface APIError { diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index c7fcbd1..a9e8a81 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -199,31 +199,11 @@ export class Pronouns extends Label { export class Field { name: string; labels: Label[]; - constructor({ - name, - favourite, - okay, - jokingly, - friends_only, - avoid, - }: API.Field) { + constructor({ name, entries }: API.Field) { this.name = name; - function transpose(arr: API.Arr, status: LabelStatus): Label[] { - return (arr ?? []).map( - (text) => - new Label({ - displayText: null, - text, - status, - }) - ); - } - this.labels = [ - ...transpose(favourite, LabelStatus.Favourite), - ...transpose(okay, LabelStatus.Okay), - ...transpose(jokingly, LabelStatus.Jokingly), - ...transpose(friends_only, LabelStatus.FriendsOnly), - ...transpose(avoid, LabelStatus.Avoid), - ]; + this.labels = + entries?.map( + (e) => new Label({ displayText: null, text: e.value, status: e.status }) + ) ?? []; } } From d6017f1edf3d673ff09ce0f2e9bb08bc9deef06d Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 31 Jan 2023 00:50:17 +0100 Subject: [PATCH 3/3] feat: read/write improved names/pronouns for users, read/write improved fields/names/pronouns for members --- backend/db/entries.go | 75 +++++++-- backend/db/member.go | 73 +++++--- backend/db/names_pronouns.go | 224 +++---------------------- backend/db/user.go | 16 +- backend/routes/member/create_member.go | 30 ++-- backend/routes/member/get_member.go | 36 +--- backend/routes/member/patch_member.go | 61 +++---- backend/routes/user/get_user.go | 20 +-- backend/routes/user/patch_user.go | 55 +++--- frontend/lib/api-fetch.ts | 7 +- frontend/lib/api.ts | 4 +- 11 files changed, 231 insertions(+), 370 deletions(-) diff --git a/backend/db/entries.go b/backend/db/entries.go index 02e234a..7110da7 100644 --- a/backend/db/entries.go +++ b/backend/db/entries.go @@ -1,6 +1,11 @@ package db -import "codeberg.org/u1f320/pronouns.cc/backend/db/queries" +import ( + "fmt" + "strings" + + "codeberg.org/u1f320/pronouns.cc/backend/db/queries" +) type WordStatus int @@ -19,12 +24,63 @@ type FieldEntry struct { Status WordStatus `json:"status"` } +func (fe FieldEntry) Validate() string { + if fe.Value == "" { + return "value cannot be empty" + } + + if len([]rune(fe.Value)) > FieldEntryMaxLength { + return fmt.Sprintf("name must be %d characters or less, is %d", FieldEntryMaxLength, len([]rune(fe.Value))) + } + + if fe.Status == StatusUnknown || fe.Status >= wordStatusMax { + return fmt.Sprintf("status is invalid, must be between 1 and %d, is %d", wordStatusMax-1, fe.Status) + } + + return "" +} + type PronounEntry struct { Pronouns string `json:"pronouns"` DisplayText *string `json:"display_text"` Status WordStatus `json:"status"` } +func (p PronounEntry) Validate() string { + if p.Pronouns == "" { + return "pronouns cannot be empty" + } + + if p.DisplayText != nil { + if len([]rune(*p.DisplayText)) > FieldEntryMaxLength { + return fmt.Sprintf("display_text must be %d characters or less, is %d", FieldEntryMaxLength, len([]rune(*p.DisplayText))) + } + } + + if len([]rune(p.Pronouns)) > FieldEntryMaxLength { + return fmt.Sprintf("pronouns must be %d characters or less, is %d", FieldEntryMaxLength, len([]rune(p.Pronouns))) + } + + if p.Status == StatusUnknown || p.Status >= wordStatusMax { + return fmt.Sprintf("status is invalid, must be between 1 and %d, is %d", wordStatusMax-1, p.Status) + } + + return "" +} + +func (p PronounEntry) String() string { + if p.DisplayText != nil { + return *p.DisplayText + } + + split := strings.Split(p.Pronouns, "/") + if len(split) <= 2 { + return strings.Join(split, "/") + } + + return strings.Join(split[:1], "/") +} + func dbEntriesToFieldEntries(entries []queries.FieldEntry) []FieldEntry { out := make([]FieldEntry, len(entries)) for i := range entries { @@ -35,22 +91,13 @@ func dbEntriesToFieldEntries(entries []queries.FieldEntry) []FieldEntry { return out } -func dbPronounEntriesToPronounEntries(entries []queries.PronounEntry) []PronounEntry { - out := make([]PronounEntry, len(entries)) - for i := range entries { - out[i] = PronounEntry{ - *entries[i].Value, entries[i].DisplayValue, WordStatus(*entries[i].Status), - } - } - return out -} - func entriesToDBEntries(entries []FieldEntry) []queries.FieldEntry { out := make([]queries.FieldEntry, len(entries)) for i := range entries { status := int32(entries[i].Status) out[i] = queries.FieldEntry{ - &entries[i].Value, &status, + Value: &entries[i].Value, + Status: &status, } } return out @@ -61,7 +108,9 @@ func pronounEntriesToDBEntries(entries []PronounEntry) []queries.PronounEntry { for i := range entries { status := int32(entries[i].Status) out[i] = queries.PronounEntry{ - &entries[i].Pronouns, entries[i].DisplayText, &status, + Value: &entries[i].Pronouns, + DisplayValue: entries[i].DisplayText, + Status: &status, } } return out diff --git a/backend/db/member.go b/backend/db/member.go index b4d2e23..b700d79 100644 --- a/backend/db/member.go +++ b/backend/db/member.go @@ -3,6 +3,7 @@ package db import ( "context" + "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "emperror.dev/errors" "github.com/georgysavva/scany/pgxscan" "github.com/jackc/pgconn" @@ -23,6 +24,8 @@ type Member struct { Bio *string AvatarURLs []string `db:"avatar_urls"` Links []string + Names []FieldEntry + Pronouns []PronounEntry } const ( @@ -30,19 +33,27 @@ const ( ErrMemberNameInUse = errors.Sentinel("member name already in use") ) -func (db *DB) getMember(ctx context.Context, q pgxscan.Querier, id xid.ID) (m Member, err error) { - sql, args, err := sq.Select("*").From("members").Where("id = ?", id).ToSql() +func (db *DB) getMember(ctx context.Context, q querier, id xid.ID) (m Member, err error) { + qm, err := queries.NewQuerier(q).GetMemberByID(ctx, id.String()) if err != nil { - return m, errors.Wrap(err, "building sql") + return m, errors.Wrap(err, "getting member from db") } - err = pgxscan.Get(ctx, q, &m, sql, args...) + userID, err := xid.FromString(qm.UserID) if err != nil { - if errors.Cause(err) == pgx.ErrNoRows { - return m, ErrMemberNotFound - } + return m, errors.Wrap(err, "parsing user ID") + } - return m, errors.Wrap(err, "retrieving member") + m = Member{ + ID: id, + UserID: userID, + Name: qm.Name, + DisplayName: qm.DisplayName, + Bio: qm.Bio, + AvatarURLs: qm.AvatarUrls, + Links: qm.Links, + Names: fieldEntriesFromDB(qm.Names), + Pronouns: pronounsFromDB(qm.Pronouns), } return m, nil } @@ -53,26 +64,35 @@ func (db *DB) Member(ctx context.Context, id xid.ID) (m Member, err error) { // UserMember returns a member scoped by user. func (db *DB) UserMember(ctx context.Context, userID xid.ID, memberRef string) (m Member, err error) { - sql, args, err := sq.Select("*").From("members"). - Where("user_id = ? and (id = ? or name = ?)", userID, memberRef, memberRef).ToSql() + qm, err := db.q.GetMemberByName(ctx, userID.String(), memberRef) if err != nil { - return m, errors.Wrap(err, "building sql") + return m, errors.Wrap(err, "getting member from db") } - err = pgxscan.Get(ctx, db, &m, sql, args...) + memberID, err := xid.FromString(qm.ID) if err != nil { - if errors.Cause(err) == pgx.ErrNoRows { - return m, ErrMemberNotFound - } + return m, errors.Wrap(err, "parsing member ID") + } - return m, errors.Wrap(err, "retrieving member") + m = Member{ + ID: memberID, + UserID: userID, + Name: qm.Name, + DisplayName: qm.DisplayName, + Bio: qm.Bio, + AvatarURLs: qm.AvatarUrls, + Links: qm.Links, + Names: fieldEntriesFromDB(qm.Names), + Pronouns: pronounsFromDB(qm.Pronouns), } return m, nil } // UserMembers returns all of a user's members, sorted by name. func (db *DB) UserMembers(ctx context.Context, userID xid.ID) (ms []Member, err error) { - sql, args, err := sq.Select("*").From("members").Where("user_id = ?", userID).OrderBy("name", "id").ToSql() + sql, args, err := sq.Select("id", "user_id", "name", "display_name", "bio", "avatar_urls"). + From("members").Where("user_id = ?", userID). + OrderBy("name", "id").ToSql() if err != nil { return nil, errors.Wrap(err, "building sql") } @@ -93,12 +113,13 @@ func (db *DB) CreateMember(ctx context.Context, tx pgx.Tx, userID xid.ID, name s sql, args, err := sq.Insert("members"). Columns("user_id", "id", "name", "display_name", "bio", "links"). Values(userID, xid.New(), name, displayName, bio, links). - Suffix("RETURNING *").ToSql() + Suffix("RETURNING id").ToSql() if err != nil { return m, errors.Wrap(err, "building sql") } - err = pgxscan.Get(ctx, tx, &m, sql, args...) + var id xid.ID + err = tx.QueryRow(ctx, sql, args...).Scan(&id) if err != nil { pge := &pgconn.PgError{} if errors.As(err, &pge) { @@ -111,6 +132,11 @@ func (db *DB) CreateMember(ctx context.Context, tx pgx.Tx, userID xid.ID, name s return m, errors.Wrap(err, "executing query") } + m, err = db.getMember(ctx, tx, id) + if err != nil { + return m, errors.Wrap(err, "getting created member") + } + return m, nil } @@ -192,12 +218,12 @@ func (db *DB) UpdateMember( } } - sql, args, err := builder.Suffix("RETURNING *").ToSql() + sql, args, err := builder.ToSql() if err != nil { return m, errors.Wrap(err, "building sql") } - err = pgxscan.Get(ctx, tx, &m, sql, args...) + _, err = tx.Exec(ctx, sql, args...) if err != nil { pge := &pgconn.PgError{} if errors.As(err, &pge) { @@ -209,5 +235,10 @@ func (db *DB) UpdateMember( return m, errors.Wrap(err, "executing sql") } + m, err = db.getMember(ctx, tx, id) + if err != nil { + return m, errors.Wrap(err, "getting member") + } + return m, nil } diff --git a/backend/db/names_pronouns.go b/backend/db/names_pronouns.go index e5d0907..c58b0ad 100644 --- a/backend/db/names_pronouns.go +++ b/backend/db/names_pronouns.go @@ -2,234 +2,52 @@ package db import ( "context" - "fmt" - "strings" "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "emperror.dev/errors" - "github.com/georgysavva/scany/pgxscan" "github.com/jackc/pgx/v4" "github.com/rs/xid" ) -type Name struct { - ID int64 `json:"-"` - Name string `json:"name"` - Status WordStatus `json:"status"` -} - -func (n Name) Validate() string { - if n.Name == "" { - return "name cannot be empty" - } - - if len([]rune(n.Name)) > FieldEntryMaxLength { - return fmt.Sprintf("name must be %d characters or less, is %d", FieldEntryMaxLength, len([]rune(n.Name))) - } - - if n.Status == StatusUnknown || n.Status >= wordStatusMax { - return fmt.Sprintf("status is invalid, must be between 1 and %d, is %d", wordStatusMax-1, n.Status) - } - - return "" -} - -type Pronoun struct { - ID int64 `json:"-"` - DisplayText *string `json:"display_text"` - Pronouns string `json:"pronouns"` - Status WordStatus `json:"status"` -} - -func (p Pronoun) Validate() string { - if p.Pronouns == "" { - return "pronouns cannot be empty" - } - - if p.DisplayText != nil { - if len([]rune(*p.DisplayText)) > FieldEntryMaxLength { - return fmt.Sprintf("display_text must be %d characters or less, is %d", FieldEntryMaxLength, len([]rune(*p.DisplayText))) - } - } - - if len([]rune(p.Pronouns)) > FieldEntryMaxLength { - return fmt.Sprintf("pronouns must be %d characters or less, is %d", FieldEntryMaxLength, len([]rune(p.Pronouns))) - } - - if p.Status == StatusUnknown || p.Status >= wordStatusMax { - return fmt.Sprintf("status is invalid, must be between 1 and %d, is %d", wordStatusMax-1, p.Status) - } - - return "" -} - -func (p Pronoun) String() string { - if p.DisplayText != nil { - return *p.DisplayText - } - - split := strings.Split(p.Pronouns, "/") - if len(split) <= 2 { - return strings.Join(split, "/") - } - - return strings.Join(split[:1], "/") -} - -func (db *DB) SetUserNames(ctx context.Context, tx pgx.Tx, userID xid.ID, names []Name) (err error) { - sql, args, err := sq.Delete("user_names").Where("user_id = ?", userID).ToSql() +func (db *DB) SetUserNamesPronouns(ctx context.Context, tx pgx.Tx, userID xid.ID, names []FieldEntry, pronouns []PronounEntry) (err error) { + _, err = queries.NewQuerier(tx).UpdateUserNamesPronouns(ctx, queries.UpdateUserNamesPronounsParams{ + ID: userID.String(), + Names: entriesToDBEntries(names), + Pronouns: pronounEntriesToDBEntries(pronouns), + }) if err != nil { - return errors.Wrap(err, "building sql") - } - - _, err = tx.Exec(ctx, sql, args...) - if err != nil { - return errors.Wrap(err, "deleting existing names") - } - - _, err = tx.CopyFrom(ctx, - pgx.Identifier{"user_names"}, - []string{"user_id", "name", "status"}, - pgx.CopyFromSlice(len(names), func(i int) ([]any, error) { - return []any{ - userID, - names[i].Name, - names[i].Status, - }, nil - })) - if err != nil { - return errors.Wrap(err, "inserting new names") + return errors.Wrap(err, "executing update names/pronouns query") } return nil } -func (db *DB) SetUserPronouns(ctx context.Context, tx pgx.Tx, userID xid.ID, names []Pronoun) (err error) { - sql, args, err := sq.Delete("user_pronouns").Where("user_id = ?", userID).ToSql() +func (db *DB) SetMemberNamesPronouns(ctx context.Context, tx pgx.Tx, memberID xid.ID, names []FieldEntry, pronouns []PronounEntry) (err error) { + _, err = queries.NewQuerier(tx).UpdateMemberNamesPronouns(ctx, queries.UpdateMemberNamesPronounsParams{ + ID: memberID.String(), + Names: entriesToDBEntries(names), + Pronouns: pronounEntriesToDBEntries(pronouns), + }) if err != nil { - return errors.Wrap(err, "building sql") - } - - _, err = tx.Exec(ctx, sql, args...) - if err != nil { - return errors.Wrap(err, "deleting existing pronouns") - } - - _, err = tx.CopyFrom(ctx, - pgx.Identifier{"user_pronouns"}, - []string{"user_id", "pronouns", "display_text", "status"}, - pgx.CopyFromSlice(len(names), func(i int) ([]any, error) { - return []any{ - userID, - names[i].Pronouns, - names[i].DisplayText, - names[i].Status, - }, nil - })) - if err != nil { - return errors.Wrap(err, "inserting new pronouns") + return errors.Wrap(err, "executing update names/pronouns query") } return nil } -func (db *DB) MemberNames(ctx context.Context, memberID xid.ID) (ns []Name, err error) { - sql, args, err := sq.Select("id", "name", "status").From("member_names").Where("member_id = ?", memberID).OrderBy("id").ToSql() - if err != nil { - return nil, errors.Wrap(err, "building sql") - } - - err = pgxscan.Select(ctx, db, &ns, sql, args...) - if err != nil { - return nil, errors.Wrap(err, "executing query") - } - return ns, nil -} - -func (db *DB) MemberPronouns(ctx context.Context, memberID xid.ID) (ps []Pronoun, err error) { - sql, args, err := sq. - Select("id", "display_text", "pronouns", "status"). - From("member_pronouns").Where("member_id = ?", memberID). - OrderBy("id").ToSql() - if err != nil { - return nil, errors.Wrap(err, "building sql") - } - - err = pgxscan.Select(ctx, db, &ps, sql, args...) - if err != nil { - return nil, errors.Wrap(err, "executing query") - } - return ps, nil -} - -func (db *DB) SetMemberNames(ctx context.Context, tx pgx.Tx, memberID xid.ID, names []Name) (err error) { - sql, args, err := sq.Delete("member_names").Where("member_id = ?", memberID).ToSql() - if err != nil { - return errors.Wrap(err, "building sql") - } - - _, err = tx.Exec(ctx, sql, args...) - if err != nil { - return errors.Wrap(err, "deleting existing names") - } - - _, err = tx.CopyFrom(ctx, - pgx.Identifier{"member_names"}, - []string{"member_id", "name", "status"}, - pgx.CopyFromSlice(len(names), func(i int) ([]any, error) { - return []any{ - memberID, - names[i].Name, - names[i].Status, - }, nil - })) - if err != nil { - return errors.Wrap(err, "inserting new names") - } - return nil -} - -func (db *DB) SetMemberPronouns(ctx context.Context, tx pgx.Tx, memberID xid.ID, names []Pronoun) (err error) { - sql, args, err := sq.Delete("member_pronouns").Where("member_id = ?", memberID).ToSql() - if err != nil { - return errors.Wrap(err, "building sql") - } - - _, err = tx.Exec(ctx, sql, args...) - if err != nil { - return errors.Wrap(err, "deleting existing pronouns") - } - - _, err = tx.CopyFrom(ctx, - pgx.Identifier{"member_pronouns"}, - []string{"member_id", "pronouns", "display_text", "status"}, - pgx.CopyFromSlice(len(names), func(i int) ([]any, error) { - return []any{ - memberID, - names[i].Pronouns, - names[i].DisplayText, - names[i].Status, - }, nil - })) - if err != nil { - return errors.Wrap(err, "inserting new pronouns") - } - return nil -} - -func namesFromDB(dn []queries.FieldEntry) []Name { - names := make([]Name, len(dn)) +func fieldEntriesFromDB(dn []queries.FieldEntry) []FieldEntry { + names := make([]FieldEntry, len(dn)) for i := range dn { - names[i] = Name{ - Name: *dn[i].Value, + names[i] = FieldEntry{ + Value: *dn[i].Value, Status: WordStatus(*dn[i].Status), } } return names } -func pronounsFromDB(dn []queries.PronounEntry) []Pronoun { - pronouns := make([]Pronoun, len(dn)) +func pronounsFromDB(dn []queries.PronounEntry) []PronounEntry { + pronouns := make([]PronounEntry, len(dn)) for i := range dn { - pronouns[i] = Pronoun{ + pronouns[i] = PronounEntry{ DisplayText: dn[i].DisplayValue, Pronouns: *dn[i].Value, Status: WordStatus(*dn[i].Status), diff --git a/backend/db/user.go b/backend/db/user.go index 4b68afe..589cbc2 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -22,8 +22,8 @@ type User struct { AvatarURLs []string `db:"avatar_urls"` Links []string - Names []Name - Pronouns []Pronoun + Names []FieldEntry + Pronouns []PronounEntry Discord *string DiscordUsername *string @@ -133,7 +133,7 @@ func (db *DB) getUser(ctx context.Context, q querier, id xid.ID) (u User, err er DisplayName: qu.DisplayName, Bio: qu.Bio, AvatarURLs: qu.AvatarUrls, - Names: namesFromDB(qu.Names), + Names: fieldEntriesFromDB(qu.Names), Pronouns: pronounsFromDB(qu.Pronouns), Links: qu.Links, Discord: qu.Discord, @@ -171,7 +171,7 @@ func (db *DB) Username(ctx context.Context, name string) (u User, err error) { DisplayName: qu.DisplayName, Bio: qu.Bio, AvatarURLs: qu.AvatarUrls, - Names: namesFromDB(qu.Names), + Names: fieldEntriesFromDB(qu.Names), Pronouns: pronounsFromDB(qu.Pronouns), Links: qu.Links, Discord: qu.Discord, @@ -260,15 +260,19 @@ func (db *DB) UpdateUser( } } - sql, args, err := builder.Suffix("RETURNING *").ToSql() + sql, args, err := builder.ToSql() if err != nil { return u, errors.Wrap(err, "building sql") } - err = pgxscan.Get(ctx, tx, &u, sql, args...) + _, err = tx.Exec(ctx, sql, args...) if err != nil { return u, errors.Wrap(err, "executing sql") } + u, err = db.getUser(ctx, tx, id) + if err != nil { + return u, errors.Wrap(err, "getting updated user") + } return u, nil } diff --git a/backend/routes/member/create_member.go b/backend/routes/member/create_member.go index 840ec12..50ff54e 100644 --- a/backend/routes/member/create_member.go +++ b/backend/routes/member/create_member.go @@ -12,14 +12,14 @@ import ( ) type CreateMemberRequest struct { - Name string `json:"name"` - DisplayName *string `json:"display_name"` - Bio string `json:"bio"` - Avatar string `json:"avatar"` - Links []string `json:"links"` - Names []db.Name `json:"names"` - Pronouns []db.Pronoun `json:"pronouns"` - Fields []db.Field `json:"fields"` + Name string `json:"name"` + DisplayName *string `json:"display_name"` + Bio string `json:"bio"` + Avatar string `json:"avatar"` + Links []string `json:"links"` + Names []db.FieldEntry `json:"names"` + Pronouns []db.PronounEntry `json:"pronouns"` + Fields []db.Field `json:"fields"` } func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error) { @@ -92,16 +92,14 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error } // set names, pronouns, fields - err = s.DB.SetMemberNames(ctx, tx, m.ID, cmr.Names) + err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, cmr.Names, cmr.Pronouns) if err != nil { - log.Errorf("setting names for member %v: %v", m.ID, err) - return err - } - err = s.DB.SetMemberPronouns(ctx, tx, m.ID, cmr.Pronouns) - if err != nil { - log.Errorf("setting pronouns for member %v: %v", m.ID, err) + log.Errorf("setting names and pronouns for member %v: %v", m.ID, err) return err } + m.Names = cmr.Names + m.Pronouns = cmr.Pronouns + err = s.DB.SetMemberFields(ctx, tx, m.ID, cmr.Fields) if err != nil { log.Errorf("setting fields for member %v: %v", m.ID, err) @@ -144,7 +142,7 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error return errors.Wrap(err, "committing transaction") } - render.JSON(w, r, dbMemberToMember(u, m, cmr.Names, cmr.Pronouns, cmr.Fields)) + render.JSON(w, r, dbMemberToMember(u, m, cmr.Fields)) return nil } diff --git a/backend/routes/member/get_member.go b/backend/routes/member/get_member.go index 0bfa1bc..8020c17 100644 --- a/backend/routes/member/get_member.go +++ b/backend/routes/member/get_member.go @@ -19,14 +19,14 @@ type GetMemberResponse struct { AvatarURLs []string `json:"avatar_urls"` Links []string `json:"links"` - Names []db.Name `json:"names"` - Pronouns []db.Pronoun `json:"pronouns"` - Fields []db.Field `json:"fields"` + Names []db.FieldEntry `json:"names"` + Pronouns []db.PronounEntry `json:"pronouns"` + Fields []db.Field `json:"fields"` User PartialUser `json:"user"` } -func dbMemberToMember(u db.User, m db.Member, names []db.Name, pronouns []db.Pronoun, fields []db.Field) GetMemberResponse { +func dbMemberToMember(u db.User, m db.Member, fields []db.Field) GetMemberResponse { return GetMemberResponse{ ID: m.ID, Name: m.Name, @@ -35,8 +35,8 @@ func dbMemberToMember(u db.User, m db.Member, names []db.Name, pronouns []db.Pro AvatarURLs: m.AvatarURLs, Links: m.Links, - Names: names, - Pronouns: pronouns, + Names: m.Names, + Pronouns: m.Pronouns, Fields: fields, User: PartialUser{ @@ -77,22 +77,12 @@ func (s *Server) getMember(w http.ResponseWriter, r *http.Request) error { return err } - names, err := s.DB.MemberNames(ctx, m.ID) - if err != nil { - return err - } - - pronouns, err := s.DB.MemberPronouns(ctx, m.ID) - if err != nil { - return err - } - fields, err := s.DB.MemberFields(ctx, m.ID) if err != nil { return err } - render.JSON(w, r, dbMemberToMember(u, m, names, pronouns, fields)) + render.JSON(w, r, dbMemberToMember(u, m, fields)) return nil } @@ -113,22 +103,12 @@ func (s *Server) getUserMember(w http.ResponseWriter, r *http.Request) error { } } - names, err := s.DB.MemberNames(ctx, m.ID) - if err != nil { - return err - } - - pronouns, err := s.DB.MemberPronouns(ctx, m.ID) - if err != nil { - return err - } - fields, err := s.DB.MemberFields(ctx, m.ID) if err != nil { return err } - render.JSON(w, r, dbMemberToMember(u, m, names, pronouns, fields)) + render.JSON(w, r, dbMemberToMember(u, m, fields)) return nil } diff --git a/backend/routes/member/patch_member.go b/backend/routes/member/patch_member.go index 1696eef..61f39d6 100644 --- a/backend/routes/member/patch_member.go +++ b/backend/routes/member/patch_member.go @@ -14,14 +14,14 @@ import ( ) type PatchMemberRequest struct { - Name *string `json:"name"` - Bio *string `json:"bio"` - DisplayName *string `json:"display_name"` - Links *[]string `json:"links"` - Names *[]db.Name `json:"names"` - Pronouns *[]db.Pronoun `json:"pronouns"` - Fields *[]db.Field `json:"fields"` - Avatar *string `json:"avatar"` + Name *string `json:"name"` + Bio *string `json:"bio"` + DisplayName *string `json:"display_name"` + Links *[]string `json:"links"` + Names *[]db.FieldEntry `json:"names"` + Pronouns *[]db.PronounEntry `json:"pronouns"` + Fields *[]db.Field `json:"fields"` + Avatar *string `json:"avatar"` } func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { @@ -169,42 +169,27 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { } - var ( - names []db.Name - pronouns []db.Pronoun - fields []db.Field - ) + if req.Names != nil || req.Pronouns != nil { + names := m.Names + pronouns := m.Pronouns - if req.Names != nil { - err = s.DB.SetMemberNames(ctx, tx, id, *req.Names) + if req.Names != nil { + names = *req.Names + } + if req.Pronouns != nil { + pronouns = *req.Pronouns + } + + err = s.DB.SetMemberNamesPronouns(ctx, tx, id, names, pronouns) if err != nil { log.Errorf("setting names for member %v: %v", id, err) return err } - names = *req.Names - } else { - names, err = s.DB.MemberNames(ctx, id) - if err != nil { - log.Errorf("getting names for member %v: %v", id, err) - return err - } - } - - if req.Pronouns != nil { - err = s.DB.SetMemberPronouns(ctx, tx, id, *req.Pronouns) - if err != nil { - log.Errorf("setting pronouns for member %v: %v", id, err) - return err - } - pronouns = *req.Pronouns - } else { - pronouns, err = s.DB.MemberPronouns(ctx, id) - if err != nil { - log.Errorf("getting fields for member %v: %v", id, err) - return err - } + m.Names = names + m.Pronouns = pronouns } + var fields []db.Field if req.Fields != nil { err = s.DB.SetMemberFields(ctx, tx, id, *req.Fields) if err != nil { @@ -232,6 +217,6 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { } // echo the updated member back on success - render.JSON(w, r, dbMemberToMember(u, m, names, pronouns, fields)) + render.JSON(w, r, dbMemberToMember(u, m, fields)) return nil } diff --git a/backend/routes/user/get_user.go b/backend/routes/user/get_user.go index 78b243c..ffbf56b 100644 --- a/backend/routes/user/get_user.go +++ b/backend/routes/user/get_user.go @@ -12,16 +12,16 @@ import ( ) type GetUserResponse struct { - ID xid.ID `json:"id"` - Username string `json:"name"` - DisplayName *string `json:"display_name"` - Bio *string `json:"bio"` - AvatarURLs []string `json:"avatar_urls"` - Links []string `json:"links"` - Names []db.Name `json:"names"` - Pronouns []db.Pronoun `json:"pronouns"` - Members []PartialMember `json:"members"` - Fields []db.Field `json:"fields"` + ID xid.ID `json:"id"` + Username string `json:"name"` + DisplayName *string `json:"display_name"` + Bio *string `json:"bio"` + AvatarURLs []string `json:"avatar_urls"` + Links []string `json:"links"` + Names []db.FieldEntry `json:"names"` + Pronouns []db.PronounEntry `json:"pronouns"` + Members []PartialMember `json:"members"` + Fields []db.Field `json:"fields"` } type GetMeResponse struct { diff --git a/backend/routes/user/patch_user.go b/backend/routes/user/patch_user.go index b46e9f7..2550b2e 100644 --- a/backend/routes/user/patch_user.go +++ b/backend/routes/user/patch_user.go @@ -12,14 +12,14 @@ import ( ) type PatchUserRequest struct { - Username *string `json:"username"` - DisplayName *string `json:"display_name"` - Bio *string `json:"bio"` - Links *[]string `json:"links"` - Names *[]db.Name `json:"names"` - Pronouns *[]db.Pronoun `json:"pronouns"` - Fields *[]db.Field `json:"fields"` - Avatar *string `json:"avatar"` + Username *string `json:"username"` + DisplayName *string `json:"display_name"` + Bio *string `json:"bio"` + Links *[]string `json:"links"` + Names *[]db.FieldEntry `json:"names"` + Pronouns *[]db.PronounEntry `json:"pronouns"` + Fields *[]db.Field `json:"fields"` + Avatar *string `json:"avatar"` } // patchUser parses a PatchUserRequest and updates the user with the given ID. @@ -159,26 +159,27 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { return err } + if req.Names != nil || req.Pronouns != nil { + names := u.Names + pronouns := u.Pronouns + + if req.Names != nil { + names = *req.Names + } + if req.Pronouns != nil { + pronouns = *req.Pronouns + } + + err = s.DB.SetUserNamesPronouns(ctx, tx, claims.UserID, names, pronouns) + if err != nil { + log.Errorf("setting names for member %v: %v", claims.UserID, err) + return err + } + u.Names = names + u.Pronouns = pronouns + } + var fields []db.Field - - if req.Names != nil { - err = s.DB.SetUserNames(ctx, tx, claims.UserID, *req.Names) - if err != nil { - log.Errorf("setting names for user %v: %v", claims.UserID, err) - return err - } - u.Names = *req.Names - } - - if req.Pronouns != nil { - err = s.DB.SetUserPronouns(ctx, tx, claims.UserID, *req.Pronouns) - if err != nil { - log.Errorf("setting pronouns for user %v: %v", claims.UserID, err) - return err - } - u.Pronouns = *req.Pronouns - } - if req.Fields != nil { err = s.DB.SetUserFields(ctx, tx, claims.UserID, *req.Fields) if err != nil { diff --git a/frontend/lib/api-fetch.ts b/frontend/lib/api-fetch.ts index 1d8f590..207d7f5 100644 --- a/frontend/lib/api-fetch.ts +++ b/frontend/lib/api-fetch.ts @@ -18,7 +18,7 @@ export type PartialMember = PartialPerson; export interface _Person extends PartialPerson { bio: string | null; links: Arr; - names: Arr; + names: Arr; pronouns: Arr; fields: Arr; } @@ -38,11 +38,6 @@ export interface MeUser extends User { discord_username: string | null; } -export interface Name { - name: string; - status: WordStatus; -} - export interface Pronoun { display_text?: string; pronouns: string; diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index a9e8a81..f5194b4 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -164,11 +164,11 @@ export class Label { } export class Name extends Label { - constructor({ name, status }: API.Name) { + constructor({ value, status }: API.FieldEntry) { super({ type: LabelType.Name, displayText: null, - text: name, + text: value, status, }); }