Stores: userstore

Learn how the github.com/dracory/userstore package manages users with composable queries and rich helpers.

Understanding github.com/dracory/userstore

The userstore package is Dracory's reusable user persistence layer. It wraps SQL databases behind a fluent, interface-driven API so your apps can manage users without bespoke SQL.


Architecture at a Glance

Component Purpose
store implementation Maintains DB handle and exposes the high-level CRUD/sync methods defined by StoreInterface. Schema is created via migration files.
user entity Dataobject-backed struct with fluent getters/setters, password helpers, and convenience methods like IsActive() and ProfileImageOrDefaultUrl().
role / userRole entities Optional role support (separate role and user_role tables) enabled via RolesEnabled. Built-in role handles: USER_ROLE_SUPERUSER, USER_ROLE_ADMINISTRATOR, USER_ROLE_MANAGER, USER_ROLE_USER.
group / userGroup entities Optional group support (separate group and user_group tables) enabled via GroupsEnabled.
UserQueryInterface Fluent builder for filtering, sorting, paginating, and counting users; validates inputs before issuing SQL.
Dependencies neat for the SQL DSL/schema/sort/ORM contracts, carbon for timestamps, str for hashing, uid for IDs.

Initialising a Store

store, err := userstore.NewStore(userstore.NewStoreOptions{
    DB:                db,                   // *sql.DB connection
    UserTableName:     "snv_users_user",     // table name to target
    RolesEnabled:      true,                 // enable separate role tables
    RoleTableName:     "snv_users_role",
    UserRoleTableName: "snv_users_user_role",
})
if err != nil {
    log.Fatal(err)
}
  • The database driver is auto-detected from the *sql.DB connection via neat.NewFromSQLDB; there is no DbDriverName option in current versions.
  • RolesEnabled requires both RoleTableName and UserRoleTableName.
  • GroupsEnabled requires both GroupTableName and UserGroupTableName.
  • Schema creation is handled by migration files in database/migrations/ (invoked via migrations.MigrateAll(app)), not by automigration. The store does not set AutomigrateEnabled: true.

Creating and Updating Users

ctx := context.Background()
user := userstore.NewUser().
    SetEmail("ada@example.com").
    SetFirstName("Ada").
    SetLastName("Lovelace").
    SetStatus(userstore.USER_STATUS_ACTIVE)

if err := store.UserCreate(ctx, user); err != nil {
    return err
}

// Update selective fields via fluent setters
user.SetBusinessName("Babbage Analytics")
if err := store.UserUpdate(ctx, user); err != nil {
    return err
}

Key helpers:

  • SetPasswordAndHash hashes via bcrypt before storing.
  • SoftDeletedAt()/UserSoftDelete mark rows with a timestamp instead of removing them; normal list queries hide soft-deleted users by default.
  • Metas, SetMeta, UpsertMetas manage arbitrary metadata as JSON.

Querying with Precision

query := userstore.NewUserQuery().
    SetStatus(userstore.USER_STATUS_ACTIVE).
    SetCreatedAtGte("2025-01-01 00:00:00").
    SetOrderBy("created_at").
    SetSortDirection("desc").
    SetLimit(20)

users, err := store.UserList(ctx, query)
count, err := store.UserCount(ctx, query)

userSelectQuery composes the SQL via neat ORM contracts and applies soft-delete filters unless SoftDeletedIncluded() is set. Column lists are configurable with SetColumns() so callers can project only the data they need.


Roles

Roles live in their own tables (since Blueprint v0.45.0), not on the user row. A role is identified by a handle; assignments are user_role join rows.

// Find (or lazily create) a role by handle
role, err := store.RoleFindByHandleOrCreate(ctx,
    userstore.USER_ROLE_ADMINISTRATOR,
    userstore.USER_STATUS_ACTIVE,
)

// Assign it to a user
_, err = store.UserRoleFindByUserIDAndRoleIDOrCreate(ctx, user.GetID(), role.GetID())

// Check membership
ok, err := store.UserHasRoles(ctx, user.GetID(), []string{role.GetID()})

// All roles for a user
roles, err := store.UserRoles(ctx, user.GetID())
  • Role/user-role queries mirror the user API: RoleList, RoleCount, RoleSoftDelete, UserRoleList, UserRoleCount, UserRoleSoftDelete, etc.
  • In the app layer, prefer helpers.UserHasActiveRole(ctx, app, authUser, userstore.USER_ROLE_ADMINISTRATOR) over checking a flag on the user entity.
  • New registrations automatically receive the USER_ROLE_USER role.
  • Groups work the same way when GroupsEnabled is set (GroupList, GroupFindByHandle, UserGroupCreate, UserGroups, ...).

  • Design Principles – understand the interface-first approach underlying userstore.
  • Entities & Interfaces – deep dive into dataobject-backed models.
  • Testing – leverage the package's SQLite support for fast, isolated tests.
  • Create Your Own Store – adapt userstore patterns for your own domain.