Help us shape the future of Go! Take the 2026 Go Developer Survey

db

package
v3.50.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: BSD-3-Clause Imports: 19 Imported by: 0

Documentation

Overview

Package db is glabs-web's persistence layer. It owns the connection pool and the queries; everything above it works with the decoded Go structs.

PostgreSQL (PG) is the only implementation. Until 2026-10-13 there was a second one on MongoDB, and web/db/storetest existed to run one suite against both — that equivalence is what made the switch a config change rather than a leap of faith. The shared types and constants still live in the unsuffixed files (courses.go, jobs.go, …) next to their *_pg.go implementations, which is also how users.go was always arranged.

Index

Constants

View Source
const (
	EventLogin         = "login"          // a user was active (throttled: at most once per window)
	EventLoginRejected = "login-rejected" // a request without an identity was refused
	EventJobScheduled  = "job-scheduled"  // an operation was queued to run later
	EventJobDone       = "job-done"
	EventJobFailed     = "job-failed"
	EventJobExpired    = "job-expired"
	EventJobCancelled  = "job-cancelled"
	EventOpDone        = "op-done" // an interactive (run-now) operation finished
	EventOpFailed      = "op-failed"
	EventCourseCreated = "course-created"
	EventCourseDeleted = "course-deleted"
	EventTokenSaved    = "token-saved"
	EventTokenDeleted  = "token-deleted"

	EventAccessRequested = "access-requested" // someone asked to be let in
	EventAccessGranted   = "access-granted"   // an admin approved a request
	EventAccessRejected  = "access-rejected"  // an admin turned a request down
	EventAccessRevoked   = "access-revoked"   // an admin withdrew an approval
	EventAccessReset     = "access-reset"     // an admin cleared a decision; the person may ask again
)

Event types. Unlike the owner-scoped activity log (which records only the six mutating GitLab ops of a single user), the event log is the platform-wide monitoring trail an operator reads: who signed in, which jobs were scheduled and how they ended, and anything else worth watching. It is deliberately NOT owner-scoped — an admin reads across all users.

View Source
const (
	SeverityInfo    = "info"
	SeverityWarning = "warning"
	SeverityError   = "error"
)

Event severities, used to highlight what matters in the digest and the admin page. They live here (not in config) to stay independent of the linter's Severity type.

View Source
const (
	JobPending   = "pending"
	JobRunning   = "running"
	JobDone      = "done"
	JobFailed    = "failed"
	JobExpired   = "expired"
	JobCancelled = "cancelled"
)

Job status values. A job starts pending, is claimed to running, and ends in exactly one terminal state.

View Source
const (
	AccessPending  = "pending"
	AccessApproved = "approved"
	AccessRejected = "rejected"
	AccessRevoked  = "revoked"
)

Access states of a row in the users table. "Never asked" has no row and no constant here; the app calls it AccessNone.

Variables

View Source
var ErrCourseNotFound = errors.New("course not found")

ErrCourseNotFound is returned when a course does not exist for the given owner. It deliberately does not distinguish "does not exist" from "belongs to someone else": to one user, another user's course simply is not there.

View Source
var ErrJobNotFound = errors.New("scheduled job not found")

ErrJobNotFound is returned when a job does not exist for the given owner (or is no longer in a state that permits the requested change).

View Source
var ErrNoDueJob = errors.New("no due job")

ErrNoDueJob is returned by ClaimDueJob when there is nothing to run.

Functions

func LooksLikeMongoURI added in v3.46.0

func LooksLikeMongoURI(uri string) bool

LooksLikeMongoURI reports whether a connection string is a MongoDB one.

This exists for exactly one moment: the evening glabs-web is switched over, when the most likely mistake is a new image starting against an unchanged .glabs-web.yaml. Without it the server would come up and behave as though every user had no courses. bootstrap refuses to start instead.

func MigratePG added in v3.46.0

func MigratePG(ctx context.Context, pool *pgxpool.Pool) error

MigratePG brings the database up to the schema this binary was built with.

Fatal on failure, unlike the EnsureXIndexes calls it replaces: a half-migrated schema means the queries compiled into this binary do not match the database, and every later error would be a confusing symptom of that one cause.

Repeating it is a no-op -- goose applies only what is missing.

func MigrationsChecksum added in v3.46.0

func MigrationsChecksum() (string, error)

MigrationsChecksum identifies the schema this binary carries. Two binaries with the same checksum expect the same tables.

internal/pgtest names its template database after it, so that editing a migration produces a fresh template instead of silently reusing a stale one -- the failure mode that otherwise costs an afternoon of confusing test results.

Types

type ActivityEntry added in v3.25.0

type ActivityEntry struct {
	Owner      string
	Course     string
	Assignment string
	Op         string
	Params     map[string]string
	// Status is the terminal outcome — "done" or "failed".
	Status string
	// Detail is a short human summary: the repository count on success, the error
	// message on failure.
	Detail string
	At     time.Time
}

ActivityEntry records one mutating operation performed through the web against an assignment: what ran, when, and how it ended. It is the web's stand-in for the shell history the CLI leaves behind — the course page reads it to show, per assignment, what has already been done (setaccess, protect, archive, delete; later generate). Ownership is strict, exactly like courses: an entry belongs to the user who caused it and no other user can see it.

type Event added in v3.40.0

type Event struct {
	At   time.Time
	Type string
	// Actor is the acting user's email (lowercased); empty for an anonymous
	// rejected login (no identity header at all).
	Actor      string
	ActorName  string
	Department string
	Course     string
	Assignment string
	Op         string
	Severity   string
	Detail     string
	JobID      string
}

Event is one thing that happened on the platform, worth an operator's attention. Most fields are optional and depend on the type: a login carries an actor (and maybe a department) but no course; a job event carries course, assignment and op. Ownership is NOT enforced — this table exists precisely to be read across all users by an admin.

type PG added in v3.46.0

type PG struct {
	// contains filtered or unexported fields
}

PG is glabs-web's PostgreSQL store. It implements the same set of methods as *DB, the MongoDB one, and web/db/storetest runs the same suite against both -- that equivalence is what makes the switch a configuration change rather than a leap of faith.

The DTOs (StoredCourse, ScheduledJob, Event, ...) and the sentinel errors are shared: they live next to the Mongo store and stay exactly as they are. Only the way they are read and written changes.

func NewPG added in v3.46.0

func NewPG(ctx context.Context, uri string) (*PG, error)

NewPG connects and verifies the connection, so a wrong URI fails at startup rather than on the first query -- the same promise db.Connect makes.

The session time zone is pinned to Europe/Berlin, but not because the application depends on it: pgx decodes timestamptz into time.Local regardless of the session setting, and time.Local is Europe/Berlin because main.go says so. Removing this line leaves the store contract suite green -- checked, not assumed.

It is here so that a psql on the host, an ad-hoc query during an incident and the GUI all print the same wall-clock time. That is worth one line; believing it is what makes the timestamps correct would not be.

func (*PG) ActivityFor added in v3.46.0

func (db *PG) ActivityFor(ctx context.Context, owner, course, assignment string) ([]*ActivityEntry, error)

ActivityFor returns the log entries of one assignment, newest first, capped.

func (*PG) AllActivityFor added in v3.46.0

func (db *PG) AllActivityFor(ctx context.Context, owner string) ([]*ActivityEntry, error)

AllActivityFor returns the owner's complete log, newest first and UNCAPPED: a dump has to be complete, and one user's trail is bounded in practice.

func (*PG) CancelJob added in v3.46.0

func (db *PG) CancelJob(ctx context.Context, owner, id string) (*ScheduledJob, error)

CancelJob cancels one of the owner's pending jobs. A job that is already running or finished cannot be cancelled, and another user's job is invisible: both cases return ErrJobNotFound.

func (*PG) ClaimDueJob added in v3.46.0

func (db *PG) ClaimDueJob(ctx context.Context, workerID string, now time.Time) (*ScheduledJob, error)

ClaimDueJob atomically claims the oldest pending job whose time has come, flipping it to running so no other runner can take it. It returns ErrNoDueJob when nothing is due.

func (*PG) CourseActivityFor added in v3.46.0

func (db *PG) CourseActivityFor(ctx context.Context, owner, course string) ([]*ActivityEntry, error)

CourseActivityFor returns the log entries across a whole course, newest first, capped.

func (*PG) CourseOf added in v3.46.0

func (db *PG) CourseOf(ctx context.Context, owner, name string) (*StoredCourse, error)

CourseOf returns one course owned by the given user, or ErrCourseNotFound.

It deliberately does not distinguish "does not exist" from "belongs to someone else": to one user, another user's course simply is not there.

func (*PG) CoursesOf added in v3.46.0

func (db *PG) CoursesOf(ctx context.Context, owner string) ([]*StoredCourse, error)

CoursesOf returns the courses owned by the given user, sorted by name.

func (*PG) DBHost added in v3.46.0

func (db *PG) DBHost() string

DBHost is host:port with everything else stripped -- no scheme, no path and in particular no credentials, because the result is shown in the admin view.

func (*PG) DeleteCourse added in v3.46.0

func (db *PG) DeleteCourse(ctx context.Context, owner, name string) error

DeleteCourse removes a course owned by the given user. Deleting a course that does not exist for that owner is ErrCourseNotFound, not a silent success.

func (*PG) DeleteUserAccess added in v3.48.0

func (db *PG) DeleteUserAccess(ctx context.Context, email string) (bool, error)

DeleteUserAccess removes the row, so the person is back to "never asked". It reports whether there was a row.

func (*PG) DeleteUserGitLabToken added in v3.46.0

func (db *PG) DeleteUserGitLabToken(ctx context.Context, owner string) error

DeleteUserGitLabToken removes only the GitLab PAT from a user's secrets; the row stays. A user who never had one is not an error.

func (*PG) Disconnect added in v3.46.0

func (db *PG) Disconnect(_ context.Context) error

Disconnect closes the pool. Named to match the Mongo store so bootstrap does not have to know which one it holds.

func (*PG) EventsBetween added in v3.46.0

func (db *PG) EventsBetween(ctx context.Context, since, until time.Time) ([]*Event, error)

EventsBetween returns every event in [since, until), oldest first -- the window the nightly digest aggregates. Cross-user by design.

func (*PG) FinishJob added in v3.46.0

func (db *PG) FinishJob(ctx context.Context, id, status, logText, errText string) error

FinishJob records a terminal state (done/failed/expired) with its log and error. An id that no longer exists matches nothing and is not an error.

func (*PG) GetUserAccess added in v3.48.0

func (db *PG) GetUserAccess(ctx context.Context, email string) (*UserAccess, error)

GetUserAccess returns the access row for an email, or nil when the person never asked.

func (*PG) GetUserSecret added in v3.46.0

func (db *PG) GetUserSecret(ctx context.Context, owner string) (*UserSecret, error)

GetUserSecret returns the stored secrets for a user, or nil when none exist.

(nil, nil) rather than a not-found error: every caller reads a nil result as "no token configured", which is the normal state on a first visit.

func (*PG) InsertAccessRequest added in v3.48.0

func (db *PG) InsertAccessRequest(ctx context.Context, u *UserAccess) (created bool, err error)

InsertAccessRequest stores a pending request. created is false when a row for the email already existed; the existing row is then left untouched, which is what makes a repeated request harmless.

func (*PG) JobOf added in v3.46.0

func (db *PG) JobOf(ctx context.Context, owner, id string) (*ScheduledJob, error)

JobOf returns one of the owner's jobs, or ErrJobNotFound.

func (*PG) JobsOf added in v3.46.0

func (db *PG) JobsOf(ctx context.Context, owner string, statuses []string) ([]*ScheduledJob, error)

JobsOf returns the owner's jobs, newest scheduled first, optionally filtered to the given statuses. Like courses, there is no read without an owner filter.

func (*PG) ListUserAccess added in v3.48.0

func (db *PG) ListUserAccess(ctx context.Context) ([]*UserAccess, error)

ListUserAccess returns every row, open requests first.

func (*PG) MarkNotified added in v3.46.0

func (db *PG) MarkNotified(ctx context.Context, id string) error

MarkNotified flags that the terminal-state email for a job has been sent, so a restart mid-notification does not send it twice.

func (*PG) MigrateSchema added in v3.46.0

func (db *PG) MigrateSchema(ctx context.Context) error

MigrateSchema brings the connected database up to the schema this binary carries. It is what the server calls at startup.

func (*PG) ReapExpired added in v3.46.0

func (db *PG) ReapExpired(ctx context.Context, now time.Time) (jobs, events int64, err error)

ReapExpired deletes what MongoDB's TTL indexes used to delete: finished jobs after 30 days, events after 180.

PostgreSQL has no TTL, and pg_cron would mean an extension with shared_preload_libraries and therefore a custom image. A delete on the runner's existing tick is less machinery and has three advantages over the TTL monitor it replaces: it is testable, it shows up in the log, and the retention period sits in Go next to the comment explaining it rather than inside an index option.

func (*PG) RecentEvents added in v3.46.0

func (db *PG) RecentEvents(ctx context.Context, since time.Time, limit int64) ([]*Event, error)

RecentEvents returns events at or after since, newest first, capped at limit -- the admin page's live feed. A limit of zero or less means no cap, matching the Mongo store. Cross-user by design.

func (*PG) RecordActivity added in v3.46.0

func (db *PG) RecordActivity(ctx context.Context, e *ActivityEntry) error

RecordActivity appends one entry to the log.

func (*PG) RecordEvent added in v3.46.0

func (db *PG) RecordEvent(ctx context.Context, e *Event) error

RecordEvent appends one event to the log. A missing severity defaults to info so a forgotten field never hides an event -- and, as before, the default is written back onto the caller's struct.

func (*PG) SaveCourse added in v3.46.0

func (db *PG) SaveCourse(ctx context.Context, course *StoredCourse) error

SaveCourse inserts or replaces a course for its owner.

func (*PG) SaveJob added in v3.46.0

func (db *PG) SaveJob(ctx context.Context, job *ScheduledJob) error

SaveJob inserts a new job, assigning it an id if it has none.

func (*PG) SaveUserGitLabToken added in v3.46.0

func (db *PG) SaveUserGitLabToken(ctx context.Context, owner string, sealed secrets.SealedValue, updatedAt time.Time) error

SaveUserGitLabToken upserts the sealed GitLab PAT for a user, touching only the gitlab columns so it never clobbers other secrets on the row.

func (*PG) SetSummarySentAt added in v3.46.0

func (db *PG) SetSummarySentAt(ctx context.Context, at time.Time) error

SetSummarySentAt records when the nightly summary was last sent.

func (*PG) SetUserAccessStatus added in v3.48.0

func (db *PG) SetUserAccessStatus(ctx context.Context, email, status, decidedBy string, decidedAt time.Time) (*UserAccess, error)

SetUserAccessStatus records an admin's decision and returns the updated row, or nil when there is no row for the email.

func (*PG) SystemState added in v3.46.0

func (db *PG) SystemState(ctx context.Context) (*SystemState, error)

SystemState returns the single state row, or a zero-valued one (never nil) if it somehow does not exist.

The migration seeds the row, so the not-found branch should be unreachable -- it stays because the caller's contract is "never nil", and the nightly digest asks for this on every tick including the first one after a fresh install.

func (*PG) UnnotifiedTerminalJobs added in v3.46.0

func (db *PG) UnnotifiedTerminalJobs(ctx context.Context) ([]*ScheduledJob, error)

UnnotifiedTerminalJobs returns finished jobs (done/failed/expired) whose notification email has not been sent yet, across all owners.

type ScheduledJob added in v3.26.0

type ScheduledJob struct {
	ID         string
	Owner      string
	Op         string
	Course     string
	Assignment string
	OnlyFor    []string
	Params     map[string]string
	RunAt      time.Time
	// ConfigHash is copied from the confirm token; the runner re-resolves and
	// compares it at fire time, refusing a job whose config drifted since planning.
	ConfigHash string
	Status     string
	GraceMin   int
	CreatedAt  time.Time
	StartedAt  *time.Time
	FinishedAt *time.Time
	Log        string
	Err        string
	Notified   bool
	WorkerID   string
}

ScheduledJob is one mutating operation queued to run at a wall-clock time. It is the persistent unit the poll-runner claims and executes; because it is stored rather than held in memory, jobs survive restarts, missed runs are caught up after downtime, and an atomic claim keeps two runners from firing the same job. Ownership is strict, exactly like courses.

type StoredCourse added in v3.2.0

type StoredCourse struct {
	Owner      string
	Name       string
	Source     *config.CourseSource
	RawYAML    []byte
	ImportedAt time.Time
	UpdatedAt  time.Time
}

StoredCourse is a course as saved by one user. Ownership is strict: a course belongs to the user who imported it, and no other user can see or touch it.

RawYAML is kept verbatim alongside the parsed Source so a download can return exactly what was uploaded — comments and key order and all — as long as the course has not been edited through the web. Re-encoding Source would lose them.

type SystemState added in v3.40.0

type SystemState struct {
	ID            string
	SummarySentAt *time.Time
}

SystemState holds server-wide bookkeeping. Today it records only when the last nightly summary was sent, so the digest window survives restarts and a summary is never sent twice for the same period.

type UserAccess added in v3.48.0

type UserAccess struct {
	Email       string
	Name        string
	Department  string
	Status      string
	Reason      string
	RequestedAt time.Time
	DecidedAt   *time.Time
	DecidedBy   string
}

UserAccess is one person who asked to use glabs-web, and what an admin decided. PostgreSQL only: there is no MongoDB implementation, as with ReapExpired.

type UserSecret added in v3.3.0

type UserSecret struct {
	Owner           string
	GitLab          *secrets.SealedValue
	GitLabUpdatedAt *time.Time
}

UserSecret holds a user's encrypted per-user secrets, keyed by the owner's email — here, the GitLab personal access token. The value is AES-256-GCM sealed; the plaintext never touches the database. This row is never exposed over GraphQL, only a "set / when" status is.

Directories

Path Synopsis
Package storetest is the contract a glabs-web store implementation has to satisfy, written once and run against it.
Package storetest is the contract a glabs-web store implementation has to satisfy, written once and run against it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL