Merge pull request 'names/pronouns/fields rework' (#17) from better-fields into main
Reviewed-on: https://codeberg.org/u1f320/pronouns.cc/pulls/17
This commit is contained in:
		
						commit
						d11f296026
					
				
					 22 changed files with 1632 additions and 556 deletions
				
			
		
							
								
								
									
										4
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								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 ./... | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										117
									
								
								backend/db/entries.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								backend/db/entries.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,117 @@ | |||
| package db | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"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"` | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 		out[i] = FieldEntry{ | ||||
| 			*entries[i].Value, 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{ | ||||
| 			Value:  &entries[i].Value, | ||||
| 			Status: &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{ | ||||
| 			Value:        &entries[i].Pronouns, | ||||
| 			DisplayValue: entries[i].DisplayText, | ||||
| 			Status:       &status, | ||||
| 		} | ||||
| 	} | ||||
| 	return out | ||||
| } | ||||
|  | @ -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") | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -2,255 +2,56 @@ 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 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"` | ||||
| 	Status WordStatus `json:"status"` | ||||
| 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, "executing update names/pronouns query") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (n Name) Validate() string { | ||||
| 	if n.Name == "" { | ||||
| 		return "name cannot be empty" | ||||
| 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, "executing update names/pronouns query") | ||||
| 	} | ||||
| 
 | ||||
| 	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 "" | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| 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))) | ||||
| func fieldEntriesFromDB(dn []queries.FieldEntry) []FieldEntry { | ||||
| 	names := make([]FieldEntry, len(dn)) | ||||
| 	for i := range dn { | ||||
| 		names[i] = FieldEntry{ | ||||
| 			Value:  *dn[i].Value, | ||||
| 			Status: WordStatus(*dn[i].Status), | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	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 "" | ||||
| 	return names | ||||
| } | ||||
| 
 | ||||
| func (p Pronoun) String() string { | ||||
| 	if p.DisplayText != nil { | ||||
| 		return *p.DisplayText | ||||
| func pronounsFromDB(dn []queries.PronounEntry) []PronounEntry { | ||||
| 	pronouns := make([]PronounEntry, len(dn)) | ||||
| 	for i := range dn { | ||||
| 		pronouns[i] = PronounEntry{ | ||||
| 			DisplayText: dn[i].DisplayValue, | ||||
| 			Pronouns:    *dn[i].Value, | ||||
| 			Status:      WordStatus(*dn[i].Status), | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	split := strings.Split(p.Pronouns, "/") | ||||
| 	if len(split) <= 2 { | ||||
| 		return strings.Join(split, "/") | ||||
| 	} | ||||
| 
 | ||||
| 	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 { | ||||
| 		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 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() | ||||
| 	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 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 | ||||
| 	return pronouns | ||||
| } | ||||
|  |  | |||
							
								
								
									
										3
									
								
								backend/db/queries/generate.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								backend/db/queries/generate.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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" | ||||
							
								
								
									
										31
									
								
								backend/db/queries/queries.member.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								backend/db/queries/queries.member.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 *; | ||||
							
								
								
									
										803
									
								
								backend/db/queries/queries.member.sql.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										803
									
								
								backend/db/queries/queries.member.sql.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
							
								
								
									
										21
									
								
								backend/db/queries/queries.user.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								backend/db/queries/queries.user.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 *; | ||||
							
								
								
									
										310
									
								
								backend/db/queries/queries.user.sql.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										310
									
								
								backend/db/queries/queries.user.sql.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
|  | @ -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" | ||||
|  | @ -21,6 +22,9 @@ type User struct { | |||
| 	AvatarURLs []string `db:"avatar_urls"` | ||||
| 	Links      []string | ||||
| 
 | ||||
| 	Names    []FieldEntry | ||||
| 	Pronouns []PronounEntry | ||||
| 
 | ||||
| 	Discord         *string | ||||
| 	DiscordUsername *string | ||||
| 
 | ||||
|  | @ -98,7 +102,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 +117,28 @@ 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, | ||||
| 		Names:           fieldEntriesFromDB(qu.Names), | ||||
| 		Pronouns:        pronounsFromDB(qu.Pronouns), | ||||
| 		Links:           qu.Links, | ||||
| 		Discord:         qu.Discord, | ||||
| 		DiscordUsername: qu.DiscordUsername, | ||||
| 		MaxInvites:      int(qu.MaxInvites), | ||||
| 	} | ||||
| 
 | ||||
| 	return u, nil | ||||
|  | @ -133,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:           fieldEntriesFromDB(qu.Names), | ||||
| 		Pronouns:        pronounsFromDB(qu.Pronouns), | ||||
| 		Links:           qu.Links, | ||||
| 		Discord:         qu.Discord, | ||||
| 		DiscordUsername: qu.DiscordUsername, | ||||
| 		MaxInvites:      int(qu.MaxInvites), | ||||
| 	} | ||||
| 
 | ||||
| 	return u, nil | ||||
|  | @ -223,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 | ||||
| } | ||||
|  |  | |||
|  | @ -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{ | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  | @ -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, | ||||
| 	}) | ||||
|  |  | |||
|  | @ -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,42 +159,27 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { | |||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	var ( | ||||
| 		names    []db.Name | ||||
| 		pronouns []db.Pronoun | ||||
| 		fields   []db.Field | ||||
| 	) | ||||
| 	if req.Names != nil || req.Pronouns != nil { | ||||
| 		names := u.Names | ||||
| 		pronouns := u.Pronouns | ||||
| 
 | ||||
| 	if req.Names != nil { | ||||
| 		err = s.DB.SetUserNames(ctx, tx, claims.UserID, *req.Names) | ||||
| 		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 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 | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| 		} | ||||
| 		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) | ||||
| 			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.Fields != nil { | ||||
| 		err = s.DB.SetUserFields(ctx, tx, claims.UserID, *req.Fields) | ||||
| 		if err != nil { | ||||
|  | @ -217,7 +202,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 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ export type PartialMember = PartialPerson; | |||
| export interface _Person extends PartialPerson { | ||||
|   bio: string | null; | ||||
|   links: Arr<string>; | ||||
|   names: Arr<Name>; | ||||
|   names: Arr<FieldEntry>; | ||||
|   pronouns: Arr<Pronoun>; | ||||
|   fields: Arr<Field>; | ||||
| } | ||||
|  | @ -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; | ||||
|  | @ -51,11 +46,12 @@ export interface Pronoun { | |||
| 
 | ||||
| export interface Field { | ||||
|   name: string; | ||||
|   favourite: Arr<string>; | ||||
|   okay: Arr<string>; | ||||
|   jokingly: Arr<string>; | ||||
|   friends_only: Arr<string>; | ||||
|   avoid: Arr<string>; | ||||
|   entries: Arr<FieldEntry>; | ||||
| } | ||||
| 
 | ||||
| export interface FieldEntry { | ||||
|   value: string; | ||||
|   status: WordStatus; | ||||
| } | ||||
| 
 | ||||
| export interface APIError { | ||||
|  |  | |||
|  | @ -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, | ||||
|     }); | ||||
|   } | ||||
|  | @ -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<string>, 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 }) | ||||
|       ) ?? []; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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 ( | ||||
|  |  | |||
							
								
								
									
										35
									
								
								scripts/migrate/004_field_arrays.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								scripts/migrate/004_field_arrays.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue