Add Manager.UsersByIDs, a bulk user lookup for the rate visitor refresh

This commit is contained in:
binwiederhier committed 2026-10-31 13:51:38 +00:00
1 parent 6ae42e7590
commit 14f631da8f
4 files changed
+39

No files matched your search

+24
View File
@@ -26,6 +26,7 @@ const (
syncTopicPrefix = "st_"
syncTopicLength = 16
userIDPrefix = "u_"
usersByIDsChunk = 1000 // IDs per statement of UsersByIDs
userIDLength = 12
userAuthIntentionalSlowDownHash = "$2a$10$YFCQvqQDwIIwnJM1xkAYOeih0dg17UVGanaTStnrSzC8NCWxcLDwy" // Cost should match DefaultUserPasswordBcryptCost
userHardDeleteAfterDuration = 7 * 24 * time.Hour
@@ -457,6 +458,29 @@ func (a *Manager) UserByID(id string) (*User, error) {
return a.readUser(rows)
}
// UsersByIDs returns the users with the given IDs, in chunks of usersByIDsChunk, skipping the IDs
// that do not exist. Deleted users are returned as such; the caller decides what to do with them.
// The IDs are passed as one JSON array, which both dialects unpack, so the statement is fixed.
func (a *Manager) UsersByIDs(ids []string) ([]*User, error) {
users := make([]*User, 0, len(ids))
for chunk := range slices.Chunk(ids, usersByIDsChunk) {
param, err := json.Marshal(chunk)
if err != nil {
return nil, err
}
rows, err := a.db.Query(a.queries.selectUsersByIDs, string(param))
if err != nil {
return nil, err
}
chunkUsers, err := a.readUsers(rows)
if err != nil {
return nil, err
}
users = append(users, chunkUsers...)
}
return users, nil
}
// UserByEmailOrUsername resolves an identifier to a single user, trying it first as a primary
// email address and then as a username. A verified, owned email takes precedence over a
// freely-chosen username, so a look-alike username cannot shadow the email's real owner. Returns
+7
View File
@@ -25,6 +25,12 @@ const (
LEFT JOIN tier t on t.id = u.tier_id
WHERE u.id = $1
`
postgresSelectUsersByIDsQuery = `
SELECT u.id, u.user_name, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, u.deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
FROM "user" u
LEFT JOIN tier t on t.id = u.tier_id
WHERE u.id IN (SELECT jsonb_array_elements_text($1::jsonb))
`
postgresSelectUserByNameQuery = `
SELECT u.id, u.user_name, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, u.deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
FROM "user" u
@@ -257,6 +263,7 @@ const (
// NewPostgresManager creates a new Manager backed by a PostgreSQL database using an existing connection pool.
var postgresQueries = queries{
selectUserByID: postgresSelectUserByIDQuery,
selectUsersByIDs: postgresSelectUsersByIDsQuery,
selectUserByName: postgresSelectUserByNameQuery,
selectUserByNameOrPrimaryEmail: postgresSelectUserByNameOrPrimaryEmailQuery,
selectUserByToken: postgresSelectUserByTokenQuery,
+7
View File
@@ -31,6 +31,12 @@ const (
LEFT JOIN tier t on t.id = u.tier_id
WHERE u.id = ?
`
sqliteSelectUsersByIDsQuery = `
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
FROM user u
LEFT JOIN tier t on t.id = u.tier_id
WHERE u.id IN (SELECT value FROM json_each(?))
`
sqliteSelectUserByNameQuery = `
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
FROM user u
@@ -255,6 +261,7 @@ const (
var sqliteQueries = queries{
selectUserByID: sqliteSelectUserByIDQuery,
selectUsersByIDs: sqliteSelectUsersByIDsQuery,
selectUserByName: sqliteSelectUserByNameQuery,
selectUserByNameOrPrimaryEmail: sqliteSelectUserByNameOrPrimaryEmailQuery,
selectUserByToken: sqliteSelectUserByTokenQuery,
+1
View File
@@ -356,6 +356,7 @@ var (
type queries struct {
// User queries
selectUserByID string
selectUsersByIDs string
selectUserByName string
selectUserByNameOrPrimaryEmail string
selectUserByToken string