From 98e6916bd3808e533765260952f851b9066dfd84 Mon Sep 17 00:00:00 2001 From: JWSametz Date: Thu, 10 Sep 2026 11:55:53 +0200 Subject: [PATCH 1/9] feat(leaderboards): add leaderboard config persistence --- .../00102_add_leaderboard_configs.sql | 31 ++ .../database/queries/leaderboard_configs.sql | 99 +++++ .../database/sqlc/leaderboard_configs.sql.go | 394 ++++++++++++++++++ backend/internal/database/sqlc/models.go | 14 + backend/internal/ulid/ulid.go | 11 + backend/internal/ulid/ulid_test.go | 1 + 6 files changed, 550 insertions(+) create mode 100644 backend/internal/database/migrations/00102_add_leaderboard_configs.sql create mode 100644 backend/internal/database/queries/leaderboard_configs.sql create mode 100644 backend/internal/database/sqlc/leaderboard_configs.sql.go diff --git a/backend/internal/database/migrations/00102_add_leaderboard_configs.sql b/backend/internal/database/migrations/00102_add_leaderboard_configs.sql new file mode 100644 index 00000000..457d1c5f --- /dev/null +++ b/backend/internal/database/migrations/00102_add_leaderboard_configs.sql @@ -0,0 +1,31 @@ +-- +goose Up +-- +goose StatementBegin + +CREATE TABLE leaderboard_configs ( + id CHAR(28) PRIMARY KEY CHECK (id ~ '^LC[0-9A-Z]{26}$'), + project_id CHAR(28) NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + event_id CHAR(28) REFERENCES events(id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + slug VARCHAR(100) NOT NULL, + entity_type VARCHAR(20) NOT NULL CHECK (entity_type IN ('PERSONS', 'TEAMS', 'SUPERTEAMS', 'CHURCHES')), + filter JSONB, + sort_order INT NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_leaderboard_configs_project ON leaderboard_configs(project_id); +CREATE INDEX idx_leaderboard_configs_event ON leaderboard_configs(event_id); +CREATE UNIQUE INDEX idx_leaderboard_configs_project_slug ON leaderboard_configs(project_id, slug); + +CREATE TRIGGER update_leaderboard_configs_updated_at BEFORE UPDATE ON leaderboard_configs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +DROP TABLE IF EXISTS leaderboard_configs; + +-- +goose StatementEnd diff --git a/backend/internal/database/queries/leaderboard_configs.sql b/backend/internal/database/queries/leaderboard_configs.sql new file mode 100644 index 00000000..8f0d6186 --- /dev/null +++ b/backend/internal/database/queries/leaderboard_configs.sql @@ -0,0 +1,99 @@ +-- name: GetLeaderboardConfigByID :one +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE id = @id::char(28); + +-- name: GetLeaderboardConfigsByIDs :many +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE id = ANY(@ids::char(28)[]); + +-- name: GetLeaderboardConfigsByProjectIDs :many +-- Returns ALL configs (including inactive) for the given project IDs. +-- Non-admin visibility filtering must be done at the application layer. +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE project_id = ANY(@project_ids::char(28)[]) +ORDER BY project_id, sort_order, id; + +-- name: GetLeaderboardConfigsByEventIDs :many +-- Returns ALL configs (including inactive) for the given event IDs. +-- Non-admin visibility filtering must be done at the application layer. +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE event_id = ANY(@event_ids::char(28)[]) +ORDER BY event_id, sort_order, id; + +-- name: GetLeaderboardConfigsFilteredCursor :many +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE + (@ids::char(28)[] IS NULL OR id = ANY(@ids::char(28)[])) + AND (@projectid::char(28) = '' OR project_id = @projectid::char(28)) + AND (@eventid::char(28) = '' OR event_id = @eventid::char(28)) + AND (sqlc.narg('isactive')::bool IS NULL OR is_active = sqlc.narg('isactive')::bool) + AND ( + @aftercursorcreatedat::timestamptz IS NULL + OR (created_at, id) < (@aftercursorcreatedat::timestamptz, @aftercursorid::text) + ) + AND ( + @beforecursorcreatedat::timestamptz IS NULL + OR (created_at, id) > (@beforecursorcreatedat::timestamptz, @beforecursorid::text) + ) +ORDER BY + CASE WHEN @isbackward::bool = true THEN created_at END ASC, + CASE WHEN @isbackward::bool = true THEN id END ASC, + CASE WHEN @isbackward::bool = false OR @isbackward::bool IS NULL THEN created_at END DESC, + CASE WHEN @isbackward::bool = false OR @isbackward::bool IS NULL THEN id END DESC +LIMIT CASE WHEN @querylimit::int IS NULL THEN NULL ELSE @querylimit::int END; + +-- name: CountLeaderboardConfigsFiltered :one +SELECT COUNT(DISTINCT id) +FROM leaderboard_configs +WHERE + (@ids::char(28)[] IS NULL OR id = ANY(@ids::char(28)[])) + AND (@projectid::char(28) = '' OR project_id = @projectid::char(28)) + AND (@eventid::char(28) = '' OR event_id = @eventid::char(28)) + AND (sqlc.narg('isactive')::bool IS NULL OR is_active = sqlc.narg('isactive')::bool); + +-- name: CreateLeaderboardConfig :one +INSERT INTO leaderboard_configs ( + id, + project_id, + event_id, + name, + slug, + entity_type, + filter, + sort_order, + is_active +) +VALUES ( + @id::text, + @projectid::text, + sqlc.narg('eventid')::text, + @name::text, + @slug::text, + @entitytype::text, + sqlc.narg('filter')::jsonb, + COALESCE(sqlc.narg('sortorder')::int, 0), + COALESCE(sqlc.narg('isactive')::bool, true) +) +RETURNING id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at; + +-- name: UpdateLeaderboardConfig :one +UPDATE leaderboard_configs +SET + name = COALESCE(sqlc.narg('name')::text, name), + slug = COALESCE(sqlc.narg('slug')::text, slug), + entity_type = COALESCE(sqlc.narg('entitytype')::text, entity_type), + filter = COALESCE(sqlc.narg('filter')::jsonb, filter), + sort_order = COALESCE(sqlc.narg('sortorder')::int, sort_order), + is_active = COALESCE(sqlc.narg('isactive')::bool, is_active), + updated_at = now() +WHERE id = @id::char(28) +RETURNING id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at; + +-- name: DeleteLeaderboardConfig :exec +DELETE FROM leaderboard_configs +WHERE id = @id::char(28); diff --git a/backend/internal/database/sqlc/leaderboard_configs.sql.go b/backend/internal/database/sqlc/leaderboard_configs.sql.go new file mode 100644 index 00000000..ed2721fd --- /dev/null +++ b/backend/internal/database/sqlc/leaderboard_configs.sql.go @@ -0,0 +1,394 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: leaderboard_configs.sql + +package sqlc + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CountLeaderboardConfigsFiltered = `-- name: CountLeaderboardConfigsFiltered :one +SELECT COUNT(DISTINCT id) +FROM leaderboard_configs +WHERE + ($1::char(28)[] IS NULL OR id = ANY($1::char(28)[])) + AND ($2::char(28) = '' OR project_id = $2::char(28)) + AND ($3::char(28) = '' OR event_id = $3::char(28)) + AND ($4::bool IS NULL OR is_active = $4::bool) +` + +type CountLeaderboardConfigsFilteredParams struct { + Ids []string `json:"ids"` + Projectid string `json:"projectid"` + Eventid string `json:"eventid"` + Isactive *bool `json:"isactive"` +} + +func (q *Queries) CountLeaderboardConfigsFiltered(ctx context.Context, arg CountLeaderboardConfigsFilteredParams) (int64, error) { + row := q.db.QueryRow(ctx, CountLeaderboardConfigsFiltered, + arg.Ids, + arg.Projectid, + arg.Eventid, + arg.Isactive, + ) + var count int64 + err := row.Scan(&count) + return count, err +} + +const CreateLeaderboardConfig = `-- name: CreateLeaderboardConfig :one +INSERT INTO leaderboard_configs ( + id, + project_id, + event_id, + name, + slug, + entity_type, + filter, + sort_order, + is_active +) +VALUES ( + $1::text, + $2::text, + $3::text, + $4::text, + $5::text, + $6::text, + $7::jsonb, + COALESCE($8::int, 0), + COALESCE($9::bool, true) +) +RETURNING id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +` + +type CreateLeaderboardConfigParams struct { + ID string `json:"id"` + Projectid string `json:"projectid"` + Eventid *string `json:"eventid"` + Name string `json:"name"` + Slug string `json:"slug"` + Entitytype string `json:"entitytype"` + Filter []byte `json:"filter"` + Sortorder *int32 `json:"sortorder"` + Isactive *bool `json:"isactive"` +} + +func (q *Queries) CreateLeaderboardConfig(ctx context.Context, arg CreateLeaderboardConfigParams) (*LeaderboardConfig, error) { + row := q.db.QueryRow(ctx, CreateLeaderboardConfig, + arg.ID, + arg.Projectid, + arg.Eventid, + arg.Name, + arg.Slug, + arg.Entitytype, + arg.Filter, + arg.Sortorder, + arg.Isactive, + ) + var i LeaderboardConfig + err := row.Scan( + &i.ID, + &i.ProjectID, + &i.EventID, + &i.Name, + &i.Slug, + &i.EntityType, + &i.Filter, + &i.SortOrder, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + ) + return &i, err +} + +const DeleteLeaderboardConfig = `-- name: DeleteLeaderboardConfig :exec +DELETE FROM leaderboard_configs +WHERE id = $1::char(28) +` + +func (q *Queries) DeleteLeaderboardConfig(ctx context.Context, id string) error { + _, err := q.db.Exec(ctx, DeleteLeaderboardConfig, id) + return err +} + +const GetLeaderboardConfigByID = `-- name: GetLeaderboardConfigByID :one +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE id = $1::char(28) +` + +func (q *Queries) GetLeaderboardConfigByID(ctx context.Context, id string) (*LeaderboardConfig, error) { + row := q.db.QueryRow(ctx, GetLeaderboardConfigByID, id) + var i LeaderboardConfig + err := row.Scan( + &i.ID, + &i.ProjectID, + &i.EventID, + &i.Name, + &i.Slug, + &i.EntityType, + &i.Filter, + &i.SortOrder, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + ) + return &i, err +} + +const GetLeaderboardConfigsByEventIDs = `-- name: GetLeaderboardConfigsByEventIDs :many +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE event_id = ANY($1::char(28)[]) +ORDER BY event_id, sort_order, id +` + +// Returns ALL configs (including inactive) for the given event IDs. +// Non-admin visibility filtering must be done at the application layer. +func (q *Queries) GetLeaderboardConfigsByEventIDs(ctx context.Context, eventIds []string) ([]*LeaderboardConfig, error) { + rows, err := q.db.Query(ctx, GetLeaderboardConfigsByEventIDs, eventIds) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*LeaderboardConfig{} + for rows.Next() { + var i LeaderboardConfig + if err := rows.Scan( + &i.ID, + &i.ProjectID, + &i.EventID, + &i.Name, + &i.Slug, + &i.EntityType, + &i.Filter, + &i.SortOrder, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetLeaderboardConfigsByIDs = `-- name: GetLeaderboardConfigsByIDs :many +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE id = ANY($1::char(28)[]) +` + +func (q *Queries) GetLeaderboardConfigsByIDs(ctx context.Context, ids []string) ([]*LeaderboardConfig, error) { + rows, err := q.db.Query(ctx, GetLeaderboardConfigsByIDs, ids) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*LeaderboardConfig{} + for rows.Next() { + var i LeaderboardConfig + if err := rows.Scan( + &i.ID, + &i.ProjectID, + &i.EventID, + &i.Name, + &i.Slug, + &i.EntityType, + &i.Filter, + &i.SortOrder, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetLeaderboardConfigsByProjectIDs = `-- name: GetLeaderboardConfigsByProjectIDs :many +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE project_id = ANY($1::char(28)[]) +ORDER BY project_id, sort_order, id +` + +// Returns ALL configs (including inactive) for the given project IDs. +// Non-admin visibility filtering must be done at the application layer. +func (q *Queries) GetLeaderboardConfigsByProjectIDs(ctx context.Context, projectIds []string) ([]*LeaderboardConfig, error) { + rows, err := q.db.Query(ctx, GetLeaderboardConfigsByProjectIDs, projectIds) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*LeaderboardConfig{} + for rows.Next() { + var i LeaderboardConfig + if err := rows.Scan( + &i.ID, + &i.ProjectID, + &i.EventID, + &i.Name, + &i.Slug, + &i.EntityType, + &i.Filter, + &i.SortOrder, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetLeaderboardConfigsFilteredCursor = `-- name: GetLeaderboardConfigsFilteredCursor :many +SELECT id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +FROM leaderboard_configs +WHERE + ($1::char(28)[] IS NULL OR id = ANY($1::char(28)[])) + AND ($2::char(28) = '' OR project_id = $2::char(28)) + AND ($3::char(28) = '' OR event_id = $3::char(28)) + AND ($4::bool IS NULL OR is_active = $4::bool) + AND ( + $5::timestamptz IS NULL + OR (created_at, id) < ($5::timestamptz, $6::text) + ) + AND ( + $7::timestamptz IS NULL + OR (created_at, id) > ($7::timestamptz, $8::text) + ) +ORDER BY + CASE WHEN $9::bool = true THEN created_at END ASC, + CASE WHEN $9::bool = true THEN id END ASC, + CASE WHEN $9::bool = false OR $9::bool IS NULL THEN created_at END DESC, + CASE WHEN $9::bool = false OR $9::bool IS NULL THEN id END DESC +LIMIT CASE WHEN $10::int IS NULL THEN NULL ELSE $10::int END +` + +type GetLeaderboardConfigsFilteredCursorParams struct { + Ids []string `json:"ids"` + Projectid string `json:"projectid"` + Eventid string `json:"eventid"` + Isactive *bool `json:"isactive"` + Aftercursorcreatedat pgtype.Timestamptz `json:"aftercursorcreatedat"` + Aftercursorid string `json:"aftercursorid"` + Beforecursorcreatedat pgtype.Timestamptz `json:"beforecursorcreatedat"` + Beforecursorid string `json:"beforecursorid"` + Isbackward bool `json:"isbackward"` + Querylimit int32 `json:"querylimit"` +} + +func (q *Queries) GetLeaderboardConfigsFilteredCursor(ctx context.Context, arg GetLeaderboardConfigsFilteredCursorParams) ([]*LeaderboardConfig, error) { + rows, err := q.db.Query(ctx, GetLeaderboardConfigsFilteredCursor, + arg.Ids, + arg.Projectid, + arg.Eventid, + arg.Isactive, + arg.Aftercursorcreatedat, + arg.Aftercursorid, + arg.Beforecursorcreatedat, + arg.Beforecursorid, + arg.Isbackward, + arg.Querylimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*LeaderboardConfig{} + for rows.Next() { + var i LeaderboardConfig + if err := rows.Scan( + &i.ID, + &i.ProjectID, + &i.EventID, + &i.Name, + &i.Slug, + &i.EntityType, + &i.Filter, + &i.SortOrder, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpdateLeaderboardConfig = `-- name: UpdateLeaderboardConfig :one +UPDATE leaderboard_configs +SET + name = COALESCE($1::text, name), + slug = COALESCE($2::text, slug), + entity_type = COALESCE($3::text, entity_type), + filter = COALESCE($4::jsonb, filter), + sort_order = COALESCE($5::int, sort_order), + is_active = COALESCE($6::bool, is_active), + updated_at = now() +WHERE id = $7::char(28) +RETURNING id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at +` + +type UpdateLeaderboardConfigParams struct { + Name *string `json:"name"` + Slug *string `json:"slug"` + Entitytype *string `json:"entitytype"` + Filter []byte `json:"filter"` + Sortorder *int32 `json:"sortorder"` + Isactive *bool `json:"isactive"` + ID string `json:"id"` +} + +func (q *Queries) UpdateLeaderboardConfig(ctx context.Context, arg UpdateLeaderboardConfigParams) (*LeaderboardConfig, error) { + row := q.db.QueryRow(ctx, UpdateLeaderboardConfig, + arg.Name, + arg.Slug, + arg.Entitytype, + arg.Filter, + arg.Sortorder, + arg.Isactive, + arg.ID, + ) + var i LeaderboardConfig + err := row.Scan( + &i.ID, + &i.ProjectID, + &i.EventID, + &i.Name, + &i.Slug, + &i.EntityType, + &i.Filter, + &i.SortOrder, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + ) + return &i, err +} diff --git a/backend/internal/database/sqlc/models.go b/backend/internal/database/sqlc/models.go index c16aea68..7c785845 100644 --- a/backend/internal/database/sqlc/models.go +++ b/backend/internal/database/sqlc/models.go @@ -242,6 +242,20 @@ type LeaderboardApplyQueue struct { ScoreAt pgtype.Timestamptz `json:"score_at"` } +type LeaderboardConfig struct { + ID string `json:"id"` + ProjectID string `json:"project_id"` + EventID *string `json:"event_id"` + Name string `json:"name"` + Slug string `json:"slug"` + EntityType string `json:"entity_type"` + Filter []byte `json:"filter"` + SortOrder int32 `json:"sort_order"` + IsActive bool `json:"is_active"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + type LeaderboardEventChurch struct { EventID string `json:"event_id"` ChurchID string `json:"church_id"` diff --git a/backend/internal/ulid/ulid.go b/backend/internal/ulid/ulid.go index fd02019c..65d69fc2 100644 --- a/backend/internal/ulid/ulid.go +++ b/backend/internal/ulid/ulid.go @@ -44,6 +44,7 @@ const ( PrefixQuizSession = "QN" // Quiz Sessions (QS taken by submissions) PrefixQuizSessionAccess = "QX" // Quiz Session Access PrefixBulkJob = "BJ" // Bulk Jobs + PrefixLeaderboardConfig = "LC" // Leaderboard Configs ) // Total ID length: 2 (prefix) + 26 (ULID) = 28 characters @@ -181,6 +182,11 @@ func NewFileUploadID() string { return newID(PrefixFileUpload) } +// NewLeaderboardConfigID generates a new ID for a leaderboard config (LC prefix) +func NewLeaderboardConfigID() string { + return newID(PrefixLeaderboardConfig) +} + // NewInstanceID generates a new instance ID (no prefix, just raw ULID) // Used for identifying server instances in distributed cache invalidation func NewInstanceID() string { @@ -427,3 +433,8 @@ func NewBulkJobID() string { func IsBulkJobID(id string) bool { return IsValidID(id, PrefixBulkJob) } + +// IsLeaderboardConfigID validates a leaderboard config ID +func IsLeaderboardConfigID(id string) bool { + return IsValidID(id, PrefixLeaderboardConfig) +} diff --git a/backend/internal/ulid/ulid_test.go b/backend/internal/ulid/ulid_test.go index e5e72d64..4c461e05 100644 --- a/backend/internal/ulid/ulid_test.go +++ b/backend/internal/ulid/ulid_test.go @@ -28,6 +28,7 @@ func TestNewIDs(t *testing.T) { {"NewContentItemID", NewContentItemID, PrefixContentItem, IsContentItemID}, {"NewScoreJournalID", NewScoreJournalID, PrefixScoreJournal, IsScoreJournalID}, {"NewUserFeedbackID", NewUserFeedbackID, PrefixUserFeedback, IsUserFeedbackID}, + {"NewLeaderboardConfigID", NewLeaderboardConfigID, PrefixLeaderboardConfig, IsLeaderboardConfigID}, } for _, tt := range tests { From bac88b741f6a0d2f1566b36bbc5e2b198b4938ab Mon Sep 17 00:00:00 2001 From: JWSametz Date: Thu, 10 Sep 2026 13:51:11 +0200 Subject: [PATCH 2/9] feat(leaderboard): leaderboards config --- .../00102_add_leaderboard_configs.sql | 4 +-- .../database/queries/leaderboard_configs.sql | 13 +++++--- .../database/sqlc/leaderboard_configs.sql.go | 33 +++++++++++-------- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/backend/internal/database/migrations/00102_add_leaderboard_configs.sql b/backend/internal/database/migrations/00102_add_leaderboard_configs.sql index 457d1c5f..94ba6329 100644 --- a/backend/internal/database/migrations/00102_add_leaderboard_configs.sql +++ b/backend/internal/database/migrations/00102_add_leaderboard_configs.sql @@ -11,8 +11,8 @@ CREATE TABLE leaderboard_configs ( filter JSONB, sort_order INT NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now() + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_leaderboard_configs_project ON leaderboard_configs(project_id); diff --git a/backend/internal/database/queries/leaderboard_configs.sql b/backend/internal/database/queries/leaderboard_configs.sql index 8f0d6186..97c54cda 100644 --- a/backend/internal/database/queries/leaderboard_configs.sql +++ b/backend/internal/database/queries/leaderboard_configs.sql @@ -34,11 +34,11 @@ WHERE AND (sqlc.narg('isactive')::bool IS NULL OR is_active = sqlc.narg('isactive')::bool) AND ( @aftercursorcreatedat::timestamptz IS NULL - OR (created_at, id) < (@aftercursorcreatedat::timestamptz, @aftercursorid::text) + OR (created_at, id) < (@aftercursorcreatedat::timestamptz, @aftercursorid::char(28)) ) AND ( @beforecursorcreatedat::timestamptz IS NULL - OR (created_at, id) > (@beforecursorcreatedat::timestamptz, @beforecursorid::text) + OR (created_at, id) > (@beforecursorcreatedat::timestamptz, @beforecursorid::char(28)) ) ORDER BY CASE WHEN @isbackward::bool = true THEN created_at END ASC, @@ -87,10 +87,13 @@ SET name = COALESCE(sqlc.narg('name')::text, name), slug = COALESCE(sqlc.narg('slug')::text, slug), entity_type = COALESCE(sqlc.narg('entitytype')::text, entity_type), - filter = COALESCE(sqlc.narg('filter')::jsonb, filter), + filter = CASE + WHEN sqlc.narg('filter')::jsonb IS NOT NULL THEN sqlc.narg('filter')::jsonb + WHEN sqlc.narg('clearfilter')::bool = true THEN NULL + ELSE filter + END, sort_order = COALESCE(sqlc.narg('sortorder')::int, sort_order), - is_active = COALESCE(sqlc.narg('isactive')::bool, is_active), - updated_at = now() + is_active = COALESCE(sqlc.narg('isactive')::bool, is_active) WHERE id = @id::char(28) RETURNING id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at; diff --git a/backend/internal/database/sqlc/leaderboard_configs.sql.go b/backend/internal/database/sqlc/leaderboard_configs.sql.go index ed2721fd..a96b9df8 100644 --- a/backend/internal/database/sqlc/leaderboard_configs.sql.go +++ b/backend/internal/database/sqlc/leaderboard_configs.sql.go @@ -272,11 +272,11 @@ WHERE AND ($4::bool IS NULL OR is_active = $4::bool) AND ( $5::timestamptz IS NULL - OR (created_at, id) < ($5::timestamptz, $6::text) + OR (created_at, id) < ($5::timestamptz, $6::char(28)) ) AND ( $7::timestamptz IS NULL - OR (created_at, id) > ($7::timestamptz, $8::text) + OR (created_at, id) > ($7::timestamptz, $8::char(28)) ) ORDER BY CASE WHEN $9::bool = true THEN created_at END ASC, @@ -348,22 +348,26 @@ SET name = COALESCE($1::text, name), slug = COALESCE($2::text, slug), entity_type = COALESCE($3::text, entity_type), - filter = COALESCE($4::jsonb, filter), - sort_order = COALESCE($5::int, sort_order), - is_active = COALESCE($6::bool, is_active), - updated_at = now() -WHERE id = $7::char(28) + filter = CASE + WHEN $4::jsonb IS NOT NULL THEN $4::jsonb + WHEN $5::bool = true THEN NULL + ELSE filter + END, + sort_order = COALESCE($6::int, sort_order), + is_active = COALESCE($7::bool, is_active) +WHERE id = $8::char(28) RETURNING id, project_id, event_id, name, slug, entity_type, filter, sort_order, is_active, created_at, updated_at ` type UpdateLeaderboardConfigParams struct { - Name *string `json:"name"` - Slug *string `json:"slug"` - Entitytype *string `json:"entitytype"` - Filter []byte `json:"filter"` - Sortorder *int32 `json:"sortorder"` - Isactive *bool `json:"isactive"` - ID string `json:"id"` + Name *string `json:"name"` + Slug *string `json:"slug"` + Entitytype *string `json:"entitytype"` + Filter []byte `json:"filter"` + Clearfilter *bool `json:"clearfilter"` + Sortorder *int32 `json:"sortorder"` + Isactive *bool `json:"isactive"` + ID string `json:"id"` } func (q *Queries) UpdateLeaderboardConfig(ctx context.Context, arg UpdateLeaderboardConfigParams) (*LeaderboardConfig, error) { @@ -372,6 +376,7 @@ func (q *Queries) UpdateLeaderboardConfig(ctx context.Context, arg UpdateLeaderb arg.Slug, arg.Entitytype, arg.Filter, + arg.Clearfilter, arg.Sortorder, arg.Isactive, arg.ID, From c6477aec75a370fba7c1ed1c20adc0815daf4b33 Mon Sep 17 00:00:00 2001 From: JWSametz Date: Thu, 10 Sep 2026 14:04:58 +0200 Subject: [PATCH 3/9] feat(graphql): define leaderboard config schema --- backend/gqlgen.yml | 15 + backend/internal/graph/api/generated.go | 3191 +++++++++++++++-- .../internal/graph/api/model/models_gen.go | 70 +- gql/events.graphqls | 2 +- gql/leaderboards.graphqls | 101 + gql/projects.graphqls | 2 +- 6 files changed, 2983 insertions(+), 398 deletions(-) create mode 100644 gql/leaderboards.graphqls diff --git a/backend/gqlgen.yml b/backend/gqlgen.yml index a86be93b..eabf8a1d 100644 --- a/backend/gqlgen.yml +++ b/backend/gqlgen.yml @@ -8,6 +8,7 @@ schema: - ../gql/achievements.graphqls - ../gql/challenges.graphqls - ../gql/streaks.graphqls + - ../gql/leaderboards.graphqls - ../gql/users.graphqls - ../gql/roles.graphqls - ../gql/churches.graphqls @@ -203,6 +204,20 @@ models: RivalCandidates: type: "[]*github.com/bcc-media/wayfarer/internal/graph/api/model.LeaderboardEntry" + LeaderboardConfig: + fields: + project: + resolver: true + event: + resolver: true + leaderboard: + resolver: true + extraFields: + ProjectID: + type: string + EventID: + type: "*string" + SimpleAchievement: fields: project: diff --git a/backend/internal/graph/api/generated.go b/backend/internal/graph/api/generated.go index b7376d0a..6a7bf341 100644 --- a/backend/internal/graph/api/generated.go +++ b/backend/internal/graph/api/generated.go @@ -50,6 +50,7 @@ type ResolverRoot interface { FreeTextResponse() FreeTextResponseResolver JsonQuestion() JsonQuestionResolver JsonResponse() JsonResponseResolver + LeaderboardConfig() LeaderboardConfigResolver LeaderboardConnection() LeaderboardConnectionResolver LeaderboardEntry() LeaderboardEntryResolver MarkdownText() MarkdownTextResolver @@ -302,6 +303,7 @@ type ComplexityRoot struct { EndDate func(childComplexity int) int ID func(childComplexity int) int Leaderboard func(childComplexity int, entityType model.LeaderboardEntityType, filter *model.LeaderboardFilter, first *int, after *string, last *int, before *string) int + Leaderboards func(childComplexity int) int Name func(childComplexity int) int ParentProject func(childComplexity int) int StartDate func(childComplexity int) int @@ -470,6 +472,32 @@ type ComplexityRoot struct { TimeSpentSeconds func(childComplexity int) int } + LeaderboardConfig struct { + CreatedAt func(childComplexity int) int + EntityType func(childComplexity int) int + Event func(childComplexity int) int + Filter func(childComplexity int) int + ID func(childComplexity int) int + IsActive func(childComplexity int) int + Leaderboard func(childComplexity int, first *int, after *string, last *int, before *string) int + Name func(childComplexity int) int + Project func(childComplexity int) int + Slug func(childComplexity int) int + SortOrder func(childComplexity int) int + UpdatedAt func(childComplexity int) int + } + + LeaderboardConfigConnection struct { + Edges func(childComplexity int) int + PageInfo func(childComplexity int) int + TotalCount func(childComplexity int) int + } + + LeaderboardConfigEdge struct { + Cursor func(childComplexity int) int + Node func(childComplexity int) int + } + LeaderboardConnection struct { Edges func(childComplexity int) int Me func(childComplexity int) int @@ -566,6 +594,7 @@ type ComplexityRoot struct { CreateContentAchievement func(childComplexity int, input model.CreateContentAchievementInput) int CreateContentAchievementFromExternalContent func(childComplexity int, input model.CreateContentAchievementFromExternalContentInput) int CreateEvent func(childComplexity int, projectID string, input model.CreateEventInput) int + CreateLeaderboardConfig func(childComplexity int, input model.CreateLeaderboardConfigInput) int CreateProject func(childComplexity int, input model.CreateProjectInput) int CreateQuiz func(childComplexity int, input model.CreateQuizInput) int CreateQuizAchievement func(childComplexity int, input model.CreateQuizAchievementInput) int @@ -582,6 +611,7 @@ type ComplexityRoot struct { DeleteChallenge func(childComplexity int, id string) int DeleteEvent func(childComplexity int, id string) int DeleteFeedback func(childComplexity int, id string) int + DeleteLeaderboardConfig func(childComplexity int, id string) int DeleteProject func(childComplexity int, id string) int DeleteQuiz func(childComplexity int, id string) int DeleteQuizQuestion func(childComplexity int, id string) int @@ -658,6 +688,7 @@ type ComplexityRoot struct { UpdateContentAchievement func(childComplexity int, id string, input model.UpdateContentAchievementInput) int UpdateEvent func(childComplexity int, id string, input model.UpdateEventInput) int UpdateFeedbackTags func(childComplexity int, feedbackID string, tags []string) int + UpdateLeaderboardConfig func(childComplexity int, id string, input model.UpdateLeaderboardConfigInput) int UpdateProject func(childComplexity int, id string, input model.UpdateProjectInput) int UpdateQuiz func(childComplexity int, id string, input model.UpdateQuizInput) int UpdateQuizAchievement func(childComplexity int, id string, input model.UpdateQuizAchievementInput) int @@ -806,6 +837,7 @@ type ComplexityRoot struct { InfoMessageStart func(childComplexity int) int Journal func(childComplexity int, filter *model.ScoreJournalFilter, first *int, after *string, last *int, before *string) int Leaderboard func(childComplexity int, entityType model.LeaderboardEntityType, filter *model.LeaderboardFilter, first *int, after *string, last *int, before *string) int + Leaderboards func(childComplexity int) int MyChurchTeams func(childComplexity int) int MyPoints func(childComplexity int) int MyTeam func(childComplexity int) int @@ -867,6 +899,8 @@ type ComplexityRoot struct { FirebaseToken func(childComplexity int) int FrontendConfig func(childComplexity int) int InstanceID func(childComplexity int) int + LeaderboardConfig func(childComplexity int, id string) int + LeaderboardConfigs func(childComplexity int, filter *model.LeaderboardConfigFilter, first *int, after *string, last *int, before *string) int Me func(childComplexity int) int MyBulkJobs func(childComplexity int, limit *int) int MyCurrentEvent func(childComplexity int) int @@ -1378,6 +1412,7 @@ type EventResolver interface { ParentProject(ctx context.Context, obj *model.Event) (*model.Project, error) TranslationStatus(ctx context.Context, obj *model.Event) ([]model.TranslationFieldStatus, error) + Leaderboards(ctx context.Context, obj *model.Event) ([]model.LeaderboardConfig, error) } type ExternalChallengeResolver interface { ImageObject(ctx context.Context, obj *model.ExternalChallenge) (*model.Image, error) @@ -1414,6 +1449,12 @@ type JsonResponseResolver interface { JournalEntry(ctx context.Context, obj *model.JSONResponse) (*model.ScoreJournal, error) } +type LeaderboardConfigResolver interface { + Project(ctx context.Context, obj *model.LeaderboardConfig) (*model.Project, error) + Event(ctx context.Context, obj *model.LeaderboardConfig) (*model.Event, error) + + Leaderboard(ctx context.Context, obj *model.LeaderboardConfig, first *int, after *string, last *int, before *string) (*model.LeaderboardConnection, error) +} type LeaderboardConnectionResolver interface { NearestChurchRivals(ctx context.Context, obj *model.LeaderboardConnection, first *int) ([]model.LeaderboardEntry, error) } @@ -1493,6 +1534,9 @@ type MutationResolver interface { BulkUnenrollUsersFromChallengeAsync(ctx context.Context, target model.EnrollmentTargetInput, challengeID string) (*model.BulkJob, error) BulkCompleteChallengesAsync(ctx context.Context, target model.EnrollmentTargetInput, challengeID string, completedAt *scalars.DateTime) (*model.BulkJob, error) BulkPublishChallengesAsync(ctx context.Context, ids []string, publishedAt scalars.DateTime) (*model.BulkJob, error) + CreateLeaderboardConfig(ctx context.Context, input model.CreateLeaderboardConfigInput) (*model.LeaderboardConfig, error) + UpdateLeaderboardConfig(ctx context.Context, id string, input model.UpdateLeaderboardConfigInput) (*model.LeaderboardConfig, error) + DeleteLeaderboardConfig(ctx context.Context, id string) (bool, error) UpdateAvatar(ctx context.Context, file graphql.Upload) (*model.User, error) AssignUserToProject(ctx context.Context, userID string, projectID string) (*model.User, error) RemoveUserFromProject(ctx context.Context, userID string, projectID string) (*model.User, error) @@ -1627,6 +1671,7 @@ type ProjectResolver interface { MyPoints(ctx context.Context, obj *model.Project) (int, error) TranslationStatus(ctx context.Context, obj *model.Project) ([]model.TranslationFieldStatus, error) + Leaderboards(ctx context.Context, obj *model.Project) ([]model.LeaderboardConfig, error) } type QueryResolver interface { Me(ctx context.Context) (*model.User, error) @@ -1651,6 +1696,8 @@ type QueryResolver interface { Achievements(ctx context.Context, filter model.AchievementFilter, first *int, after *string, last *int, before *string) (*model.AchievementConnection, error) Challenge(ctx context.Context, id string) (model.Challenge, error) Challenges(ctx context.Context, filter *model.ChallengeFilter, first *int, after *string, last *int, before *string) (*model.ChallengeConnection, error) + LeaderboardConfig(ctx context.Context, id string) (*model.LeaderboardConfig, error) + LeaderboardConfigs(ctx context.Context, filter *model.LeaderboardConfigFilter, first *int, after *string, last *int, before *string) (*model.LeaderboardConfigConnection, error) User(ctx context.Context, id string) (*model.User, error) Users(ctx context.Context, filter *model.UserFilter, first *int, after *string, last *int, before *string) (*model.UserConnection, error) UserRoles(ctx context.Context, userID string) ([]model.UserRole, error) @@ -2747,6 +2794,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Event.Leaderboard(childComplexity, args["entityType"].(model.LeaderboardEntityType), args["filter"].(*model.LeaderboardFilter), args["first"].(*int), args["after"].(*string), args["last"].(*int), args["before"].(*string)), true + case "Event.leaderboards": + if e.complexity.Event.Leaderboards == nil { + break + } + + return e.complexity.Event.Leaderboards(childComplexity), true case "Event.name": if e.complexity.Event.Name == nil { break @@ -3455,6 +3508,116 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.JsonResponse.TimeSpentSeconds(childComplexity), true + case "LeaderboardConfig.createdAt": + if e.complexity.LeaderboardConfig.CreatedAt == nil { + break + } + + return e.complexity.LeaderboardConfig.CreatedAt(childComplexity), true + case "LeaderboardConfig.entityType": + if e.complexity.LeaderboardConfig.EntityType == nil { + break + } + + return e.complexity.LeaderboardConfig.EntityType(childComplexity), true + case "LeaderboardConfig.event": + if e.complexity.LeaderboardConfig.Event == nil { + break + } + + return e.complexity.LeaderboardConfig.Event(childComplexity), true + case "LeaderboardConfig.filter": + if e.complexity.LeaderboardConfig.Filter == nil { + break + } + + return e.complexity.LeaderboardConfig.Filter(childComplexity), true + case "LeaderboardConfig.id": + if e.complexity.LeaderboardConfig.ID == nil { + break + } + + return e.complexity.LeaderboardConfig.ID(childComplexity), true + case "LeaderboardConfig.isActive": + if e.complexity.LeaderboardConfig.IsActive == nil { + break + } + + return e.complexity.LeaderboardConfig.IsActive(childComplexity), true + case "LeaderboardConfig.leaderboard": + if e.complexity.LeaderboardConfig.Leaderboard == nil { + break + } + + args, err := ec.field_LeaderboardConfig_leaderboard_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.LeaderboardConfig.Leaderboard(childComplexity, args["first"].(*int), args["after"].(*string), args["last"].(*int), args["before"].(*string)), true + case "LeaderboardConfig.name": + if e.complexity.LeaderboardConfig.Name == nil { + break + } + + return e.complexity.LeaderboardConfig.Name(childComplexity), true + case "LeaderboardConfig.project": + if e.complexity.LeaderboardConfig.Project == nil { + break + } + + return e.complexity.LeaderboardConfig.Project(childComplexity), true + case "LeaderboardConfig.slug": + if e.complexity.LeaderboardConfig.Slug == nil { + break + } + + return e.complexity.LeaderboardConfig.Slug(childComplexity), true + case "LeaderboardConfig.sortOrder": + if e.complexity.LeaderboardConfig.SortOrder == nil { + break + } + + return e.complexity.LeaderboardConfig.SortOrder(childComplexity), true + case "LeaderboardConfig.updatedAt": + if e.complexity.LeaderboardConfig.UpdatedAt == nil { + break + } + + return e.complexity.LeaderboardConfig.UpdatedAt(childComplexity), true + + case "LeaderboardConfigConnection.edges": + if e.complexity.LeaderboardConfigConnection.Edges == nil { + break + } + + return e.complexity.LeaderboardConfigConnection.Edges(childComplexity), true + case "LeaderboardConfigConnection.pageInfo": + if e.complexity.LeaderboardConfigConnection.PageInfo == nil { + break + } + + return e.complexity.LeaderboardConfigConnection.PageInfo(childComplexity), true + case "LeaderboardConfigConnection.totalCount": + if e.complexity.LeaderboardConfigConnection.TotalCount == nil { + break + } + + return e.complexity.LeaderboardConfigConnection.TotalCount(childComplexity), true + + case "LeaderboardConfigEdge.cursor": + if e.complexity.LeaderboardConfigEdge.Cursor == nil { + break + } + + return e.complexity.LeaderboardConfigEdge.Cursor(childComplexity), true + case "LeaderboardConfigEdge.node": + if e.complexity.LeaderboardConfigEdge.Node == nil { + break + } + + return e.complexity.LeaderboardConfigEdge.Node(childComplexity), true + case "LeaderboardConnection.edges": if e.complexity.LeaderboardConnection.Edges == nil { break @@ -4015,6 +4178,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.CreateEvent(childComplexity, args["projectId"].(string), args["input"].(model.CreateEventInput)), true + case "Mutation.createLeaderboardConfig": + if e.complexity.Mutation.CreateLeaderboardConfig == nil { + break + } + + args, err := ec.field_Mutation_createLeaderboardConfig_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateLeaderboardConfig(childComplexity, args["input"].(model.CreateLeaderboardConfigInput)), true case "Mutation.createProject": if e.complexity.Mutation.CreateProject == nil { break @@ -4191,6 +4365,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.DeleteFeedback(childComplexity, args["id"].(string)), true + case "Mutation.deleteLeaderboardConfig": + if e.complexity.Mutation.DeleteLeaderboardConfig == nil { + break + } + + args, err := ec.field_Mutation_deleteLeaderboardConfig_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteLeaderboardConfig(childComplexity, args["id"].(string)), true case "Mutation.deleteProject": if e.complexity.Mutation.DeleteProject == nil { break @@ -5012,6 +5197,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.UpdateFeedbackTags(childComplexity, args["feedbackId"].(string), args["tags"].([]string)), true + case "Mutation.updateLeaderboardConfig": + if e.complexity.Mutation.UpdateLeaderboardConfig == nil { + break + } + + args, err := ec.field_Mutation_updateLeaderboardConfig_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateLeaderboardConfig(childComplexity, args["id"].(string), args["input"].(model.UpdateLeaderboardConfigInput)), true case "Mutation.updateProject": if e.complexity.Mutation.UpdateProject == nil { break @@ -5807,6 +6003,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Project.Leaderboard(childComplexity, args["entityType"].(model.LeaderboardEntityType), args["filter"].(*model.LeaderboardFilter), args["first"].(*int), args["after"].(*string), args["last"].(*int), args["before"].(*string)), true + case "Project.leaderboards": + if e.complexity.Project.Leaderboards == nil { + break + } + + return e.complexity.Project.Leaderboards(childComplexity), true case "Project.myChurchTeams": if e.complexity.Project.MyChurchTeams == nil { break @@ -6178,6 +6380,28 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Query.InstanceID(childComplexity), true + case "Query.leaderboardConfig": + if e.complexity.Query.LeaderboardConfig == nil { + break + } + + args, err := ec.field_Query_leaderboardConfig_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.LeaderboardConfig(childComplexity, args["id"].(string)), true + case "Query.leaderboardConfigs": + if e.complexity.Query.LeaderboardConfigs == nil { + break + } + + args, err := ec.field_Query_leaderboardConfigs_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.LeaderboardConfigs(childComplexity, args["filter"].(*model.LeaderboardConfigFilter), args["first"].(*int), args["after"].(*string), args["last"].(*int), args["before"].(*string)), true case "Query.me": if e.complexity.Query.Me == nil { break @@ -8509,6 +8733,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputCreateContentAchievementFromExternalContentInput, ec.unmarshalInputCreateContentAchievementInput, ec.unmarshalInputCreateEventInput, + ec.unmarshalInputCreateLeaderboardConfigInput, ec.unmarshalInputCreateOrderingItemInput, ec.unmarshalInputCreatePredefinedAnswerInput, ec.unmarshalInputCreateProjectInput, @@ -8530,6 +8755,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputExternalContentFilter, ec.unmarshalInputFeedbackFilter, ec.unmarshalInputGrantQuizSessionAccessInput, + ec.unmarshalInputLeaderboardConfigFilter, ec.unmarshalInputLeaderboardFilter, ec.unmarshalInputProjectFilter, ec.unmarshalInputQuizFilter, @@ -8548,6 +8774,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputUpdateChurchInput, ec.unmarshalInputUpdateContentAchievementInput, ec.unmarshalInputUpdateEventInput, + ec.unmarshalInputUpdateLeaderboardConfigInput, ec.unmarshalInputUpdateProjectInput, ec.unmarshalInputUpdateQuizAchievementInput, ec.unmarshalInputUpdateQuizAnswerInput, @@ -8939,7 +9166,7 @@ type Project { after: String last: Int before: String - ): LeaderboardConnection! @goField(forceResolver: true) + ): LeaderboardConnection! @goField(forceResolver: true) @deprecated(reason: "Use ` + "`" + `leaderboards` + "`" + ` (LeaderboardConfig) for persisted, admin-managed leaderboards instead.") events: [Event!]! @goField(forceResolver: true) startDate: DateTime! endDate: DateTime! @@ -9047,7 +9274,7 @@ type Event { after: String last: Int before: String - ): LeaderboardConnection! @goField(forceResolver: true) + ): LeaderboardConnection! @goField(forceResolver: true) @deprecated(reason: "Use ` + "`" + `leaderboards` + "`" + ` (LeaderboardConfig) for persisted, admin-managed leaderboards instead.") startDate: DateTime! endDate: DateTime! parentProject: Project! @goField(forceResolver: true) @@ -9840,6 +10067,108 @@ extend type Mutation { bulkCompleteChallengesAsync(target: EnrollmentTargetInput!, challengeId: ID!, completedAt: DateTime): BulkJob! @requireRole(roles: ["m2m", "admin", "superadmin"]) bulkPublishChallengesAsync(ids: [ID!]!, publishedAt: DateTime!): BulkJob! @requireRole(roles: ["admin", "superadmin"]) } +`, BuiltIn: false}, + {Name: "../../../../gql/leaderboards.graphqls", Input: `# Persisted, admin-managed leaderboard definitions + +# ==================== LeaderboardConfig Type ==================== + +type LeaderboardConfig { + id: ID! + project: Project! @goField(forceResolver: true) + event: Event @goField(forceResolver: true) + name: String! + slug: String! + entityType: LeaderboardEntityType! + """ + The filter applied to this leaderboard, mirroring the ` + "`" + `LeaderboardFilter` + "`" + ` input shape. + """ + filter: JSON + sortOrder: Int! + isActive: Boolean! + createdAt: DateTime! + updatedAt: DateTime! + """ + The finished, computed leaderboard for this config. + """ + leaderboard(first: Int, after: String, last: Int, before: String): LeaderboardConnection! @goField(forceResolver: true) +} + +# ==================== Input Types ==================== + +input CreateLeaderboardConfigInput { + projectId: ID! + eventId: ID + name: String! + slug: String! + entityType: LeaderboardEntityType! + filter: LeaderboardFilter + sortOrder: Int + isActive: Boolean +} + +input UpdateLeaderboardConfigInput { + name: String + slug: String + entityType: LeaderboardEntityType + filter: LeaderboardFilter + """ + Set to true to remove the existing filter entirely (show an unfiltered leaderboard). + Ignored if ` + "`" + `filter` + "`" + ` is also provided. Has no effect otherwise. + """ + clearFilter: Boolean + sortOrder: Int + isActive: Boolean +} + +input LeaderboardConfigFilter { + projectId: ID + eventId: ID + isActive: Boolean + ids: [ID!] +} + +# ==================== Pagination ==================== + +type LeaderboardConfigEdge { + cursor: String! + node: LeaderboardConfig! +} + +type LeaderboardConfigConnection { + edges: [LeaderboardConfigEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +# ==================== Queries ==================== + +extend type Query { + # Admin management — includes inactive/draft configs. + leaderboardConfig(id: ID!): LeaderboardConfig! @requireRole(roles: ["admin", "superadmin"]) + leaderboardConfigs(filter: LeaderboardConfigFilter, first: Int, after: String, last: Int, before: String): LeaderboardConfigConnection! @requireRole(roles: ["admin", "superadmin"]) +} + +extend type Project { + """ + Active leaderboard configs for this project (all configs, including inactive, for admins/superadmins). + """ + leaderboards: [LeaderboardConfig!]! @goField(forceResolver: true) +} + +extend type Event { + """ + Active leaderboard configs for this event (all configs, including inactive, for admins/superadmins). + """ + leaderboards: [LeaderboardConfig!]! @goField(forceResolver: true) +} + +# ==================== Mutations ==================== + +extend type Mutation { + createLeaderboardConfig(input: CreateLeaderboardConfigInput!): LeaderboardConfig! @requireRole(roles: ["admin", "superadmin"]) + updateLeaderboardConfig(id: ID!, input: UpdateLeaderboardConfigInput!): LeaderboardConfig! @requireRole(roles: ["admin", "superadmin"]) + deleteLeaderboardConfig(id: ID!): Boolean! @requireRole(roles: ["admin", "superadmin"]) +} `, BuiltIn: false}, {Name: "../../../../gql/users.graphqls", Input: `# User queries and mutations @@ -11421,6 +11750,32 @@ func (ec *executionContext) field_Event_leaderboard_args(ctx context.Context, ra return args, nil } +func (ec *executionContext) field_LeaderboardConfig_leaderboard_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "first", ec.unmarshalOInt2ᚖint) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "after", ec.unmarshalOString2ᚖstring) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "last", ec.unmarshalOInt2ᚖint) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "before", ec.unmarshalOString2ᚖstring) + if err != nil { + return nil, err + } + args["before"] = arg3 + return args, nil +} + func (ec *executionContext) field_LeaderboardConnection_nearestChurchRivals_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -11978,6 +12333,17 @@ func (ec *executionContext) field_Mutation_createEvent_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Mutation_createLeaderboardConfig_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNCreateLeaderboardConfigInput2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐCreateLeaderboardConfigInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_createProject_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -12179,6 +12545,17 @@ func (ec *executionContext) field_Mutation_deleteFeedback_args(ctx context.Conte return args, nil } +func (ec *executionContext) field_Mutation_deleteLeaderboardConfig_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", ec.unmarshalNID2string) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_deleteProject_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -13187,6 +13564,22 @@ func (ec *executionContext) field_Mutation_updateFeedbackTags_args(ctx context.C return args, nil } +func (ec *executionContext) field_Mutation_updateLeaderboardConfig_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", ec.unmarshalNID2string) + if err != nil { + return nil, err + } + args["id"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNUpdateLeaderboardConfigInput2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐUpdateLeaderboardConfigInput) + if err != nil { + return nil, err + } + args["input"] = arg1 + return args, nil +} + func (ec *executionContext) field_Mutation_updateProject_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -13798,6 +14191,48 @@ func (ec *executionContext) field_Query_fileUpload_args(ctx context.Context, raw return args, nil } +func (ec *executionContext) field_Query_leaderboardConfig_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", ec.unmarshalNID2string) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_leaderboardConfigs_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOLeaderboardConfigFilter2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigFilter) + if err != nil { + return nil, err + } + args["filter"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "first", ec.unmarshalOInt2ᚖint) + if err != nil { + return nil, err + } + args["first"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "after", ec.unmarshalOString2ᚖstring) + if err != nil { + return nil, err + } + args["after"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "last", ec.unmarshalOInt2ᚖint) + if err != nil { + return nil, err + } + args["last"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "before", ec.unmarshalOString2ᚖstring) + if err != nil { + return nil, err + } + args["before"] = arg4 + return args, nil +} + func (ec *executionContext) field_Query_myBulkJobs_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -18105,6 +18540,8 @@ func (ec *executionContext) fieldContext_ContentAchievement_project(_ context.Co return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -18154,6 +18591,8 @@ func (ec *executionContext) fieldContext_ContentAchievement_event(_ context.Cont return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -18953,6 +19392,8 @@ func (ec *executionContext) fieldContext_Event_parentProject(_ context.Context, return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -18995,6 +19436,61 @@ func (ec *executionContext) fieldContext_Event_translationStatus(_ context.Conte return fc, nil } +func (ec *executionContext) _Event_leaderboards(ctx context.Context, field graphql.CollectedField, obj *model.Event) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Event_leaderboards, + func(ctx context.Context) (any, error) { + return ec.resolvers.Event().Leaderboards(ctx, obj) + }, + nil, + ec.marshalNLeaderboardConfig2ᚕgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigᚄ, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Event_leaderboards(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Event", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_LeaderboardConfig_id(ctx, field) + case "project": + return ec.fieldContext_LeaderboardConfig_project(ctx, field) + case "event": + return ec.fieldContext_LeaderboardConfig_event(ctx, field) + case "name": + return ec.fieldContext_LeaderboardConfig_name(ctx, field) + case "slug": + return ec.fieldContext_LeaderboardConfig_slug(ctx, field) + case "entityType": + return ec.fieldContext_LeaderboardConfig_entityType(ctx, field) + case "filter": + return ec.fieldContext_LeaderboardConfig_filter(ctx, field) + case "sortOrder": + return ec.fieldContext_LeaderboardConfig_sortOrder(ctx, field) + case "isActive": + return ec.fieldContext_LeaderboardConfig_isActive(ctx, field) + case "createdAt": + return ec.fieldContext_LeaderboardConfig_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_LeaderboardConfig_updatedAt(ctx, field) + case "leaderboard": + return ec.fieldContext_LeaderboardConfig_leaderboard(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type LeaderboardConfig", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _EventConnection_edges(ctx context.Context, field graphql.CollectedField, obj *model.EventConnection) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -19169,6 +19665,8 @@ func (ec *executionContext) fieldContext_EventEdge_node(_ context.Context, field return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -19403,6 +19901,8 @@ func (ec *executionContext) fieldContext_ExternalChallenge_project(_ context.Con return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -19452,6 +19952,8 @@ func (ec *executionContext) fieldContext_ExternalChallenge_event(_ context.Conte return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -22654,6 +23156,639 @@ func (ec *executionContext) fieldContext_JsonResponse_jsonResponse(_ context.Con return fc, nil } +func (ec *executionContext) _LeaderboardConfig_id(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_id, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + ec.marshalNID2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_project(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_project, + func(ctx context.Context) (any, error) { + return ec.resolvers.LeaderboardConfig().Project(ctx, obj) + }, + nil, + ec.marshalNProject2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐProject, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_project(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Project_id(ctx, field) + case "name": + return ec.fieldContext_Project_name(ctx, field) + case "description": + return ec.fieldContext_Project_description(ctx, field) + case "rules": + return ec.fieldContext_Project_rules(ctx, field) + case "infoMessage": + return ec.fieldContext_Project_infoMessage(ctx, field) + case "infoMessageStart": + return ec.fieldContext_Project_infoMessageStart(ctx, field) + case "infoMessageEnd": + return ec.fieldContext_Project_infoMessageEnd(ctx, field) + case "challenges": + return ec.fieldContext_Project_challenges(ctx, field) + case "activeChallenges": + return ec.fieldContext_Project_activeChallenges(ctx, field) + case "completedChallenges": + return ec.fieldContext_Project_completedChallenges(ctx, field) + case "activeChallengesCount": + return ec.fieldContext_Project_activeChallengesCount(ctx, field) + case "leaderboard": + return ec.fieldContext_Project_leaderboard(ctx, field) + case "events": + return ec.fieldContext_Project_events(ctx, field) + case "startDate": + return ec.fieldContext_Project_startDate(ctx, field) + case "endDate": + return ec.fieldContext_Project_endDate(ctx, field) + case "branding": + return ec.fieldContext_Project_branding(ctx, field) + case "teams": + return ec.fieldContext_Project_teams(ctx, field) + case "myChurchTeams": + return ec.fieldContext_Project_myChurchTeams(ctx, field) + case "myTeam": + return ec.fieldContext_Project_myTeam(ctx, field) + case "achievements": + return ec.fieldContext_Project_achievements(ctx, field) + case "journal": + return ec.fieldContext_Project_journal(ctx, field) + case "myPoints": + return ec.fieldContext_Project_myPoints(ctx, field) + case "archivedAt": + return ec.fieldContext_Project_archivedAt(ctx, field) + case "translationStatus": + return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_event(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_event, + func(ctx context.Context) (any, error) { + return ec.resolvers.LeaderboardConfig().Event(ctx, obj) + }, + nil, + ec.marshalOEvent2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐEvent, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_event(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Event_id(ctx, field) + case "name": + return ec.fieldContext_Event_name(ctx, field) + case "description": + return ec.fieldContext_Event_description(ctx, field) + case "challenges": + return ec.fieldContext_Event_challenges(ctx, field) + case "leaderboard": + return ec.fieldContext_Event_leaderboard(ctx, field) + case "startDate": + return ec.fieldContext_Event_startDate(ctx, field) + case "endDate": + return ec.fieldContext_Event_endDate(ctx, field) + case "parentProject": + return ec.fieldContext_Event_parentProject(ctx, field) + case "translationStatus": + return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_name(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_name, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_slug(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_slug, + func(ctx context.Context) (any, error) { + return obj.Slug, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_slug(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_entityType(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_entityType, + func(ctx context.Context) (any, error) { + return obj.EntityType, nil + }, + nil, + ec.marshalNLeaderboardEntityType2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardEntityType, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_entityType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type LeaderboardEntityType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_filter(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_filter, + func(ctx context.Context) (any, error) { + return obj.Filter, nil + }, + nil, + ec.marshalOJSON2ᚖstring, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_filter(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type JSON does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_sortOrder(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_sortOrder, + func(ctx context.Context) (any, error) { + return obj.SortOrder, nil + }, + nil, + ec.marshalNInt2int, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_sortOrder(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_isActive(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_isActive, + func(ctx context.Context) (any, error) { + return obj.IsActive, nil + }, + nil, + ec.marshalNBoolean2bool, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_isActive(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_createdAt, + func(ctx context.Context) (any, error) { + return obj.CreatedAt, nil + }, + nil, + ec.marshalNDateTime2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋscalarsᚐDateTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type DateTime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_updatedAt(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_updatedAt, + func(ctx context.Context) (any, error) { + return obj.UpdatedAt, nil + }, + nil, + ec.marshalNDateTime2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋscalarsᚐDateTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type DateTime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfig_leaderboard(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfig_leaderboard, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.LeaderboardConfig().Leaderboard(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*string), fc.Args["last"].(*int), fc.Args["before"].(*string)) + }, + nil, + ec.marshalNLeaderboardConnection2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConnection, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfig_leaderboard(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfig", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "edges": + return ec.fieldContext_LeaderboardConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_LeaderboardConnection_pageInfo(ctx, field) + case "totalCount": + return ec.fieldContext_LeaderboardConnection_totalCount(ctx, field) + case "me": + return ec.fieldContext_LeaderboardConnection_me(ctx, field) + case "nearestChurchRivals": + return ec.fieldContext_LeaderboardConnection_nearestChurchRivals(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type LeaderboardConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_LeaderboardConfig_leaderboard_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfigConnection_edges(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfigConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfigConnection_edges, + func(ctx context.Context) (any, error) { + return obj.Edges, nil + }, + nil, + ec.marshalNLeaderboardConfigEdge2ᚕgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigEdgeᚄ, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfigConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfigConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "cursor": + return ec.fieldContext_LeaderboardConfigEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_LeaderboardConfigEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type LeaderboardConfigEdge", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfigConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfigConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfigConnection_pageInfo, + func(ctx context.Context) (any, error) { + return obj.PageInfo, nil + }, + nil, + ec.marshalNPageInfo2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐPageInfo, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfigConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfigConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "hasNextPage": + return ec.fieldContext_PageInfo_hasNextPage(ctx, field) + case "hasPreviousPage": + return ec.fieldContext_PageInfo_hasPreviousPage(ctx, field) + case "startCursor": + return ec.fieldContext_PageInfo_startCursor(ctx, field) + case "endCursor": + return ec.fieldContext_PageInfo_endCursor(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfigConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfigConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfigConnection_totalCount, + func(ctx context.Context) (any, error) { + return obj.TotalCount, nil + }, + nil, + ec.marshalNInt2int, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfigConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfigConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfigEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfigEdge) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfigEdge_cursor, + func(ctx context.Context) (any, error) { + return obj.Cursor, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfigEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfigEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LeaderboardConfigEdge_node(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConfigEdge) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_LeaderboardConfigEdge_node, + func(ctx context.Context) (any, error) { + return obj.Node, nil + }, + nil, + ec.marshalNLeaderboardConfig2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfig, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_LeaderboardConfigEdge_node(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LeaderboardConfigEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_LeaderboardConfig_id(ctx, field) + case "project": + return ec.fieldContext_LeaderboardConfig_project(ctx, field) + case "event": + return ec.fieldContext_LeaderboardConfig_event(ctx, field) + case "name": + return ec.fieldContext_LeaderboardConfig_name(ctx, field) + case "slug": + return ec.fieldContext_LeaderboardConfig_slug(ctx, field) + case "entityType": + return ec.fieldContext_LeaderboardConfig_entityType(ctx, field) + case "filter": + return ec.fieldContext_LeaderboardConfig_filter(ctx, field) + case "sortOrder": + return ec.fieldContext_LeaderboardConfig_sortOrder(ctx, field) + case "isActive": + return ec.fieldContext_LeaderboardConfig_isActive(ctx, field) + case "createdAt": + return ec.fieldContext_LeaderboardConfig_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_LeaderboardConfig_updatedAt(ctx, field) + case "leaderboard": + return ec.fieldContext_LeaderboardConfig_leaderboard(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type LeaderboardConfig", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _LeaderboardConnection_edges(ctx context.Context, field graphql.CollectedField, obj *model.LeaderboardConnection) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -23967,6 +25102,8 @@ func (ec *executionContext) fieldContext_Mutation_joinProject(ctx context.Contex return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -24076,6 +25213,8 @@ func (ec *executionContext) fieldContext_Mutation_createProject(ctx context.Cont return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -24185,6 +25324,8 @@ func (ec *executionContext) fieldContext_Mutation_updateProject(ctx context.Cont return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -24364,6 +25505,8 @@ func (ec *executionContext) fieldContext_Mutation_joinEvent(ctx context.Context, return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -24443,6 +25586,8 @@ func (ec *executionContext) fieldContext_Mutation_createEvent(ctx context.Contex return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -24522,6 +25667,8 @@ func (ec *executionContext) fieldContext_Mutation_updateEvent(ctx context.Contex return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -24660,6 +25807,8 @@ func (ec *executionContext) fieldContext_Mutation_moveEvent(ctx context.Context, return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -28600,34 +29749,283 @@ func (ec *executionContext) fieldContext_Mutation_bulkEnrollUsersInChallengeAsyn } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_bulkEnrollUsersInChallengeAsync_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_bulkEnrollUsersInChallengeAsync_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_bulkUnenrollUsersFromChallengeAsync(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_bulkUnenrollUsersFromChallengeAsync, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().BulkUnenrollUsersFromChallengeAsync(ctx, fc.Args["target"].(model.EnrollmentTargetInput), fc.Args["challengeId"].(string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"admin", "superadmin", "m2m"}) + if err != nil { + var zeroVal *model.BulkJob + return zeroVal, err + } + if ec.directives.RequireRole == nil { + var zeroVal *model.BulkJob + return zeroVal, errors.New("directive requireRole is not implemented") + } + return ec.directives.RequireRole(ctx, nil, directive0, roles) + } + + next = directive1 + return next + }, + ec.marshalNBulkJob2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐBulkJob, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Mutation_bulkUnenrollUsersFromChallengeAsync(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_BulkJob_id(ctx, field) + case "operationType": + return ec.fieldContext_BulkJob_operationType(ctx, field) + case "status": + return ec.fieldContext_BulkJob_status(ctx, field) + case "totalCount": + return ec.fieldContext_BulkJob_totalCount(ctx, field) + case "processedCount": + return ec.fieldContext_BulkJob_processedCount(ctx, field) + case "successCount": + return ec.fieldContext_BulkJob_successCount(ctx, field) + case "failureCount": + return ec.fieldContext_BulkJob_failureCount(ctx, field) + case "errorMessage": + return ec.fieldContext_BulkJob_errorMessage(ctx, field) + case "createdAt": + return ec.fieldContext_BulkJob_createdAt(ctx, field) + case "startedAt": + return ec.fieldContext_BulkJob_startedAt(ctx, field) + case "completedAt": + return ec.fieldContext_BulkJob_completedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type BulkJob", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_bulkUnenrollUsersFromChallengeAsync_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_bulkCompleteChallengesAsync(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_bulkCompleteChallengesAsync, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().BulkCompleteChallengesAsync(ctx, fc.Args["target"].(model.EnrollmentTargetInput), fc.Args["challengeId"].(string), fc.Args["completedAt"].(*scalars.DateTime)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"m2m", "admin", "superadmin"}) + if err != nil { + var zeroVal *model.BulkJob + return zeroVal, err + } + if ec.directives.RequireRole == nil { + var zeroVal *model.BulkJob + return zeroVal, errors.New("directive requireRole is not implemented") + } + return ec.directives.RequireRole(ctx, nil, directive0, roles) + } + + next = directive1 + return next + }, + ec.marshalNBulkJob2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐBulkJob, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Mutation_bulkCompleteChallengesAsync(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_BulkJob_id(ctx, field) + case "operationType": + return ec.fieldContext_BulkJob_operationType(ctx, field) + case "status": + return ec.fieldContext_BulkJob_status(ctx, field) + case "totalCount": + return ec.fieldContext_BulkJob_totalCount(ctx, field) + case "processedCount": + return ec.fieldContext_BulkJob_processedCount(ctx, field) + case "successCount": + return ec.fieldContext_BulkJob_successCount(ctx, field) + case "failureCount": + return ec.fieldContext_BulkJob_failureCount(ctx, field) + case "errorMessage": + return ec.fieldContext_BulkJob_errorMessage(ctx, field) + case "createdAt": + return ec.fieldContext_BulkJob_createdAt(ctx, field) + case "startedAt": + return ec.fieldContext_BulkJob_startedAt(ctx, field) + case "completedAt": + return ec.fieldContext_BulkJob_completedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type BulkJob", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_bulkCompleteChallengesAsync_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_bulkPublishChallengesAsync(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_bulkPublishChallengesAsync, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().BulkPublishChallengesAsync(ctx, fc.Args["ids"].([]string), fc.Args["publishedAt"].(scalars.DateTime)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"admin", "superadmin"}) + if err != nil { + var zeroVal *model.BulkJob + return zeroVal, err + } + if ec.directives.RequireRole == nil { + var zeroVal *model.BulkJob + return zeroVal, errors.New("directive requireRole is not implemented") + } + return ec.directives.RequireRole(ctx, nil, directive0, roles) + } + + next = directive1 + return next + }, + ec.marshalNBulkJob2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐBulkJob, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Mutation_bulkPublishChallengesAsync(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_BulkJob_id(ctx, field) + case "operationType": + return ec.fieldContext_BulkJob_operationType(ctx, field) + case "status": + return ec.fieldContext_BulkJob_status(ctx, field) + case "totalCount": + return ec.fieldContext_BulkJob_totalCount(ctx, field) + case "processedCount": + return ec.fieldContext_BulkJob_processedCount(ctx, field) + case "successCount": + return ec.fieldContext_BulkJob_successCount(ctx, field) + case "failureCount": + return ec.fieldContext_BulkJob_failureCount(ctx, field) + case "errorMessage": + return ec.fieldContext_BulkJob_errorMessage(ctx, field) + case "createdAt": + return ec.fieldContext_BulkJob_createdAt(ctx, field) + case "startedAt": + return ec.fieldContext_BulkJob_startedAt(ctx, field) + case "completedAt": + return ec.fieldContext_BulkJob_completedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type BulkJob", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_bulkPublishChallengesAsync_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } return fc, nil } -func (ec *executionContext) _Mutation_bulkUnenrollUsersFromChallengeAsync(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_createLeaderboardConfig(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, - ec.fieldContext_Mutation_bulkUnenrollUsersFromChallengeAsync, + ec.fieldContext_Mutation_createLeaderboardConfig, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Mutation().BulkUnenrollUsersFromChallengeAsync(ctx, fc.Args["target"].(model.EnrollmentTargetInput), fc.Args["challengeId"].(string)) + return ec.resolvers.Mutation().CreateLeaderboardConfig(ctx, fc.Args["input"].(model.CreateLeaderboardConfigInput)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next directive1 := func(ctx context.Context) (any, error) { - roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"admin", "superadmin", "m2m"}) + roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"admin", "superadmin"}) if err != nil { - var zeroVal *model.BulkJob + var zeroVal *model.LeaderboardConfig return zeroVal, err } if ec.directives.RequireRole == nil { - var zeroVal *model.BulkJob + var zeroVal *model.LeaderboardConfig return zeroVal, errors.New("directive requireRole is not implemented") } return ec.directives.RequireRole(ctx, nil, directive0, roles) @@ -28636,13 +30034,13 @@ func (ec *executionContext) _Mutation_bulkUnenrollUsersFromChallengeAsync(ctx co next = directive1 return next }, - ec.marshalNBulkJob2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐBulkJob, + ec.marshalNLeaderboardConfig2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfig, true, true, ) } -func (ec *executionContext) fieldContext_Mutation_bulkUnenrollUsersFromChallengeAsync(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_createLeaderboardConfig(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -28651,29 +30049,31 @@ func (ec *executionContext) fieldContext_Mutation_bulkUnenrollUsersFromChallenge Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_BulkJob_id(ctx, field) - case "operationType": - return ec.fieldContext_BulkJob_operationType(ctx, field) - case "status": - return ec.fieldContext_BulkJob_status(ctx, field) - case "totalCount": - return ec.fieldContext_BulkJob_totalCount(ctx, field) - case "processedCount": - return ec.fieldContext_BulkJob_processedCount(ctx, field) - case "successCount": - return ec.fieldContext_BulkJob_successCount(ctx, field) - case "failureCount": - return ec.fieldContext_BulkJob_failureCount(ctx, field) - case "errorMessage": - return ec.fieldContext_BulkJob_errorMessage(ctx, field) + return ec.fieldContext_LeaderboardConfig_id(ctx, field) + case "project": + return ec.fieldContext_LeaderboardConfig_project(ctx, field) + case "event": + return ec.fieldContext_LeaderboardConfig_event(ctx, field) + case "name": + return ec.fieldContext_LeaderboardConfig_name(ctx, field) + case "slug": + return ec.fieldContext_LeaderboardConfig_slug(ctx, field) + case "entityType": + return ec.fieldContext_LeaderboardConfig_entityType(ctx, field) + case "filter": + return ec.fieldContext_LeaderboardConfig_filter(ctx, field) + case "sortOrder": + return ec.fieldContext_LeaderboardConfig_sortOrder(ctx, field) + case "isActive": + return ec.fieldContext_LeaderboardConfig_isActive(ctx, field) case "createdAt": - return ec.fieldContext_BulkJob_createdAt(ctx, field) - case "startedAt": - return ec.fieldContext_BulkJob_startedAt(ctx, field) - case "completedAt": - return ec.fieldContext_BulkJob_completedAt(ctx, field) + return ec.fieldContext_LeaderboardConfig_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_LeaderboardConfig_updatedAt(ctx, field) + case "leaderboard": + return ec.fieldContext_LeaderboardConfig_leaderboard(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type BulkJob", field.Name) + return nil, fmt.Errorf("no field named %q was found under type LeaderboardConfig", field.Name) }, } defer func() { @@ -28683,34 +30083,34 @@ func (ec *executionContext) fieldContext_Mutation_bulkUnenrollUsersFromChallenge } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_bulkUnenrollUsersFromChallengeAsync_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_createLeaderboardConfig_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } return fc, nil } -func (ec *executionContext) _Mutation_bulkCompleteChallengesAsync(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_updateLeaderboardConfig(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, - ec.fieldContext_Mutation_bulkCompleteChallengesAsync, + ec.fieldContext_Mutation_updateLeaderboardConfig, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Mutation().BulkCompleteChallengesAsync(ctx, fc.Args["target"].(model.EnrollmentTargetInput), fc.Args["challengeId"].(string), fc.Args["completedAt"].(*scalars.DateTime)) + return ec.resolvers.Mutation().UpdateLeaderboardConfig(ctx, fc.Args["id"].(string), fc.Args["input"].(model.UpdateLeaderboardConfigInput)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next directive1 := func(ctx context.Context) (any, error) { - roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"m2m", "admin", "superadmin"}) + roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"admin", "superadmin"}) if err != nil { - var zeroVal *model.BulkJob + var zeroVal *model.LeaderboardConfig return zeroVal, err } if ec.directives.RequireRole == nil { - var zeroVal *model.BulkJob + var zeroVal *model.LeaderboardConfig return zeroVal, errors.New("directive requireRole is not implemented") } return ec.directives.RequireRole(ctx, nil, directive0, roles) @@ -28719,13 +30119,13 @@ func (ec *executionContext) _Mutation_bulkCompleteChallengesAsync(ctx context.Co next = directive1 return next }, - ec.marshalNBulkJob2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐBulkJob, + ec.marshalNLeaderboardConfig2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfig, true, true, ) } -func (ec *executionContext) fieldContext_Mutation_bulkCompleteChallengesAsync(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_updateLeaderboardConfig(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -28734,29 +30134,31 @@ func (ec *executionContext) fieldContext_Mutation_bulkCompleteChallengesAsync(ct Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_BulkJob_id(ctx, field) - case "operationType": - return ec.fieldContext_BulkJob_operationType(ctx, field) - case "status": - return ec.fieldContext_BulkJob_status(ctx, field) - case "totalCount": - return ec.fieldContext_BulkJob_totalCount(ctx, field) - case "processedCount": - return ec.fieldContext_BulkJob_processedCount(ctx, field) - case "successCount": - return ec.fieldContext_BulkJob_successCount(ctx, field) - case "failureCount": - return ec.fieldContext_BulkJob_failureCount(ctx, field) - case "errorMessage": - return ec.fieldContext_BulkJob_errorMessage(ctx, field) + return ec.fieldContext_LeaderboardConfig_id(ctx, field) + case "project": + return ec.fieldContext_LeaderboardConfig_project(ctx, field) + case "event": + return ec.fieldContext_LeaderboardConfig_event(ctx, field) + case "name": + return ec.fieldContext_LeaderboardConfig_name(ctx, field) + case "slug": + return ec.fieldContext_LeaderboardConfig_slug(ctx, field) + case "entityType": + return ec.fieldContext_LeaderboardConfig_entityType(ctx, field) + case "filter": + return ec.fieldContext_LeaderboardConfig_filter(ctx, field) + case "sortOrder": + return ec.fieldContext_LeaderboardConfig_sortOrder(ctx, field) + case "isActive": + return ec.fieldContext_LeaderboardConfig_isActive(ctx, field) case "createdAt": - return ec.fieldContext_BulkJob_createdAt(ctx, field) - case "startedAt": - return ec.fieldContext_BulkJob_startedAt(ctx, field) - case "completedAt": - return ec.fieldContext_BulkJob_completedAt(ctx, field) + return ec.fieldContext_LeaderboardConfig_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_LeaderboardConfig_updatedAt(ctx, field) + case "leaderboard": + return ec.fieldContext_LeaderboardConfig_leaderboard(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type BulkJob", field.Name) + return nil, fmt.Errorf("no field named %q was found under type LeaderboardConfig", field.Name) }, } defer func() { @@ -28766,22 +30168,22 @@ func (ec *executionContext) fieldContext_Mutation_bulkCompleteChallengesAsync(ct } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_bulkCompleteChallengesAsync_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_updateLeaderboardConfig_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } return fc, nil } -func (ec *executionContext) _Mutation_bulkPublishChallengesAsync(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_deleteLeaderboardConfig(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, - ec.fieldContext_Mutation_bulkPublishChallengesAsync, + ec.fieldContext_Mutation_deleteLeaderboardConfig, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Mutation().BulkPublishChallengesAsync(ctx, fc.Args["ids"].([]string), fc.Args["publishedAt"].(scalars.DateTime)) + return ec.resolvers.Mutation().DeleteLeaderboardConfig(ctx, fc.Args["id"].(string)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next @@ -28789,11 +30191,11 @@ func (ec *executionContext) _Mutation_bulkPublishChallengesAsync(ctx context.Con directive1 := func(ctx context.Context) (any, error) { roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"admin", "superadmin"}) if err != nil { - var zeroVal *model.BulkJob + var zeroVal bool return zeroVal, err } if ec.directives.RequireRole == nil { - var zeroVal *model.BulkJob + var zeroVal bool return zeroVal, errors.New("directive requireRole is not implemented") } return ec.directives.RequireRole(ctx, nil, directive0, roles) @@ -28802,44 +30204,20 @@ func (ec *executionContext) _Mutation_bulkPublishChallengesAsync(ctx context.Con next = directive1 return next }, - ec.marshalNBulkJob2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐBulkJob, + ec.marshalNBoolean2bool, true, true, ) } -func (ec *executionContext) fieldContext_Mutation_bulkPublishChallengesAsync(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_deleteLeaderboardConfig(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_BulkJob_id(ctx, field) - case "operationType": - return ec.fieldContext_BulkJob_operationType(ctx, field) - case "status": - return ec.fieldContext_BulkJob_status(ctx, field) - case "totalCount": - return ec.fieldContext_BulkJob_totalCount(ctx, field) - case "processedCount": - return ec.fieldContext_BulkJob_processedCount(ctx, field) - case "successCount": - return ec.fieldContext_BulkJob_successCount(ctx, field) - case "failureCount": - return ec.fieldContext_BulkJob_failureCount(ctx, field) - case "errorMessage": - return ec.fieldContext_BulkJob_errorMessage(ctx, field) - case "createdAt": - return ec.fieldContext_BulkJob_createdAt(ctx, field) - case "startedAt": - return ec.fieldContext_BulkJob_startedAt(ctx, field) - case "completedAt": - return ec.fieldContext_BulkJob_completedAt(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type BulkJob", field.Name) + return nil, errors.New("field of type Boolean does not have child fields") }, } defer func() { @@ -28849,7 +30227,7 @@ func (ec *executionContext) fieldContext_Mutation_bulkPublishChallengesAsync(ctx } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_bulkPublishChallengesAsync_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_deleteLeaderboardConfig_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -35577,6 +36955,8 @@ func (ec *executionContext) fieldContext_PluginChallenge_project(_ context.Conte return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -35626,6 +37006,8 @@ func (ec *executionContext) fieldContext_PluginChallenge_event(_ context.Context return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -37274,6 +38656,8 @@ func (ec *executionContext) fieldContext_Project_events(_ context.Context, field return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -37706,6 +39090,61 @@ func (ec *executionContext) fieldContext_Project_translationStatus(_ context.Con return fc, nil } +func (ec *executionContext) _Project_leaderboards(ctx context.Context, field graphql.CollectedField, obj *model.Project) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Project_leaderboards, + func(ctx context.Context) (any, error) { + return ec.resolvers.Project().Leaderboards(ctx, obj) + }, + nil, + ec.marshalNLeaderboardConfig2ᚕgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigᚄ, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Project_leaderboards(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Project", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_LeaderboardConfig_id(ctx, field) + case "project": + return ec.fieldContext_LeaderboardConfig_project(ctx, field) + case "event": + return ec.fieldContext_LeaderboardConfig_event(ctx, field) + case "name": + return ec.fieldContext_LeaderboardConfig_name(ctx, field) + case "slug": + return ec.fieldContext_LeaderboardConfig_slug(ctx, field) + case "entityType": + return ec.fieldContext_LeaderboardConfig_entityType(ctx, field) + case "filter": + return ec.fieldContext_LeaderboardConfig_filter(ctx, field) + case "sortOrder": + return ec.fieldContext_LeaderboardConfig_sortOrder(ctx, field) + case "isActive": + return ec.fieldContext_LeaderboardConfig_isActive(ctx, field) + case "createdAt": + return ec.fieldContext_LeaderboardConfig_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_LeaderboardConfig_updatedAt(ctx, field) + case "leaderboard": + return ec.fieldContext_LeaderboardConfig_leaderboard(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type LeaderboardConfig", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _ProjectConnection_edges(ctx context.Context, field graphql.CollectedField, obj *model.ProjectConnection) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -37910,6 +39349,8 @@ func (ec *executionContext) fieldContext_ProjectEdge_node(_ context.Context, fie return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -38274,6 +39715,8 @@ func (ec *executionContext) fieldContext_Query_project(ctx context.Context, fiel return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -38413,6 +39856,8 @@ func (ec *executionContext) fieldContext_Query_myProjects(_ context.Context, fie return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -38492,6 +39937,8 @@ func (ec *executionContext) fieldContext_Query_myCurrentProject(_ context.Contex return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -38571,6 +40018,8 @@ func (ec *executionContext) fieldContext_Query_currentProject(_ context.Context, return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -38621,6 +40070,8 @@ func (ec *executionContext) fieldContext_Query_event(ctx context.Context, field return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -38731,6 +40182,8 @@ func (ec *executionContext) fieldContext_Query_myEvents(ctx context.Context, fie return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -38791,6 +40244,8 @@ func (ec *executionContext) fieldContext_Query_myCurrentEvent(_ context.Context, return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -38840,6 +40295,8 @@ func (ec *executionContext) fieldContext_Query_currentEvent(_ context.Context, f return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -39310,6 +40767,158 @@ func (ec *executionContext) fieldContext_Query_challenges(ctx context.Context, f return fc, nil } +func (ec *executionContext) _Query_leaderboardConfig(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Query_leaderboardConfig, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Query().LeaderboardConfig(ctx, fc.Args["id"].(string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"admin", "superadmin"}) + if err != nil { + var zeroVal *model.LeaderboardConfig + return zeroVal, err + } + if ec.directives.RequireRole == nil { + var zeroVal *model.LeaderboardConfig + return zeroVal, errors.New("directive requireRole is not implemented") + } + return ec.directives.RequireRole(ctx, nil, directive0, roles) + } + + next = directive1 + return next + }, + ec.marshalNLeaderboardConfig2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfig, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Query_leaderboardConfig(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_LeaderboardConfig_id(ctx, field) + case "project": + return ec.fieldContext_LeaderboardConfig_project(ctx, field) + case "event": + return ec.fieldContext_LeaderboardConfig_event(ctx, field) + case "name": + return ec.fieldContext_LeaderboardConfig_name(ctx, field) + case "slug": + return ec.fieldContext_LeaderboardConfig_slug(ctx, field) + case "entityType": + return ec.fieldContext_LeaderboardConfig_entityType(ctx, field) + case "filter": + return ec.fieldContext_LeaderboardConfig_filter(ctx, field) + case "sortOrder": + return ec.fieldContext_LeaderboardConfig_sortOrder(ctx, field) + case "isActive": + return ec.fieldContext_LeaderboardConfig_isActive(ctx, field) + case "createdAt": + return ec.fieldContext_LeaderboardConfig_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_LeaderboardConfig_updatedAt(ctx, field) + case "leaderboard": + return ec.fieldContext_LeaderboardConfig_leaderboard(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type LeaderboardConfig", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_leaderboardConfig_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_leaderboardConfigs(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Query_leaderboardConfigs, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Query().LeaderboardConfigs(ctx, fc.Args["filter"].(*model.LeaderboardConfigFilter), fc.Args["first"].(*int), fc.Args["after"].(*string), fc.Args["last"].(*int), fc.Args["before"].(*string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + roles, err := ec.unmarshalNString2ᚕstringᚄ(ctx, []any{"admin", "superadmin"}) + if err != nil { + var zeroVal *model.LeaderboardConfigConnection + return zeroVal, err + } + if ec.directives.RequireRole == nil { + var zeroVal *model.LeaderboardConfigConnection + return zeroVal, errors.New("directive requireRole is not implemented") + } + return ec.directives.RequireRole(ctx, nil, directive0, roles) + } + + next = directive1 + return next + }, + ec.marshalNLeaderboardConfigConnection2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigConnection, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Query_leaderboardConfigs(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "edges": + return ec.fieldContext_LeaderboardConfigConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_LeaderboardConfigConnection_pageInfo(ctx, field) + case "totalCount": + return ec.fieldContext_LeaderboardConfigConnection_totalCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type LeaderboardConfigConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_leaderboardConfigs_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -42078,6 +43687,8 @@ func (ec *executionContext) fieldContext_Quiz_project(_ context.Context, field g return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -43043,6 +44654,8 @@ func (ec *executionContext) fieldContext_QuizAchievement_project(_ context.Conte return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -43092,6 +44705,8 @@ func (ec *executionContext) fieldContext_QuizAchievement_event(_ context.Context return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -43666,6 +45281,8 @@ func (ec *executionContext) fieldContext_QuizChallenge_project(_ context.Context return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -43715,6 +45332,8 @@ func (ec *executionContext) fieldContext_QuizChallenge_event(_ context.Context, return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -46126,6 +47745,8 @@ func (ec *executionContext) fieldContext_RoleScope_project(_ context.Context, fi return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -46285,6 +47906,8 @@ func (ec *executionContext) fieldContext_ScoreJournal_project(_ context.Context, return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -46409,6 +48032,8 @@ func (ec *executionContext) fieldContext_ScoreJournal_event(_ context.Context, f return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -47319,6 +48944,8 @@ func (ec *executionContext) fieldContext_SimpleAchievement_project(_ context.Con return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -47368,6 +48995,8 @@ func (ec *executionContext) fieldContext_SimpleAchievement_event(_ context.Conte return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -47811,6 +49440,8 @@ func (ec *executionContext) fieldContext_SimpleChallenge_project(_ context.Conte return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -47860,6 +49491,8 @@ func (ec *executionContext) fieldContext_SimpleChallenge_event(_ context.Context return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -48574,6 +50207,8 @@ func (ec *executionContext) fieldContext_StreakAchievement_project(_ context.Con return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -48623,6 +50258,8 @@ func (ec *executionContext) fieldContext_StreakAchievement_event(_ context.Conte return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -49284,6 +50921,8 @@ func (ec *executionContext) fieldContext_SuperTeam_parentProject(_ context.Conte return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -50079,6 +51718,8 @@ func (ec *executionContext) fieldContext_Team_parentProject(_ context.Context, f return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -51073,6 +52714,8 @@ func (ec *executionContext) fieldContext_User_projects(_ context.Context, field return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -51122,6 +52765,8 @@ func (ec *executionContext) fieldContext_User_events(_ context.Context, field gr return ec.fieldContext_Event_parentProject(ctx, field) case "translationStatus": return ec.fieldContext_Event_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Event_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) }, @@ -52881,6 +54526,8 @@ func (ec *executionContext) fieldContext_Webhook_project(_ context.Context, fiel return ec.fieldContext_Project_archivedAt(ctx, field) case "translationStatus": return ec.fieldContext_Project_translationStatus(ctx, field) + case "leaderboards": + return ec.fieldContext_Project_leaderboards(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -55794,27 +57441,138 @@ func (ec *executionContext) unmarshalInputCreateContentAchievementFromExternalCo return it, err } it.Hidden = data - case "externalContentIds": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("externalContentIds")) - data, err := ec.unmarshalNID2ᚕstringᚄ(ctx, v) + case "externalContentIds": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("externalContentIds")) + data, err := ec.unmarshalNID2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.ExternalContentIds = data + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputCreateContentAchievementInput(ctx context.Context, obj any) (model.CreateContentAchievementInput, error) { + var it model.CreateContentAchievementInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"name", "descriptionPending", "descriptionCompleted", "notificationText", "imagePending", "imageCompleted", "projectId", "eventId", "challengeId", "points", "hidden", "awardableFrom", "items"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "descriptionPending": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("descriptionPending")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DescriptionPending = data + case "descriptionCompleted": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("descriptionCompleted")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DescriptionCompleted = data + case "notificationText": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("notificationText")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.NotificationText = data + case "imagePending": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("imagePending")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ImagePending = data + case "imageCompleted": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("imageCompleted")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ImageCompleted = data + case "projectId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) + data, err := ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + it.ProjectID = data + case "eventId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("eventId")) + data, err := ec.unmarshalOID2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.EventID = data + case "challengeId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("challengeId")) + data, err := ec.unmarshalOID2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.ChallengeID = data + case "points": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("points")) + data, err := ec.unmarshalNInt2int(ctx, v) + if err != nil { + return it, err + } + it.Points = data + case "hidden": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("hidden")) + data, err := ec.unmarshalNBoolean2bool(ctx, v) + if err != nil { + return it, err + } + it.Hidden = data + case "awardableFrom": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("awardableFrom")) + data, err := ec.unmarshalODateTime2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋscalarsᚐDateTime(ctx, v) if err != nil { return it, err } - it.ExternalContentIds = data + it.AwardableFrom = data + case "items": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("items")) + data, err := ec.unmarshalNContentItemInput2ᚕgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐContentItemInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Items = data } } return it, nil } -func (ec *executionContext) unmarshalInputCreateContentAchievementInput(ctx context.Context, obj any) (model.CreateContentAchievementInput, error) { - var it model.CreateContentAchievementInput +func (ec *executionContext) unmarshalInputCreateEventInput(ctx context.Context, obj any) (model.CreateEventInput, error) { + var it model.CreateEventInput asMap := map[string]any{} for k, v := range obj.(map[string]any) { asMap[k] = v } - fieldsInOrder := [...]string{"name", "descriptionPending", "descriptionCompleted", "notificationText", "imagePending", "imageCompleted", "projectId", "eventId", "challengeId", "points", "hidden", "awardableFrom", "items"} + fieldsInOrder := [...]string{"name", "description", "startDate", "endDate"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -55828,110 +57586,61 @@ func (ec *executionContext) unmarshalInputCreateContentAchievementInput(ctx cont return it, err } it.Name = data - case "descriptionPending": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("descriptionPending")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.DescriptionPending = data - case "descriptionCompleted": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("descriptionCompleted")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.DescriptionCompleted = data - case "notificationText": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("notificationText")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.NotificationText = data - case "imagePending": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("imagePending")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.ImagePending = data - case "imageCompleted": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("imageCompleted")) + case "description": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description")) data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.ImageCompleted = data - case "projectId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) - data, err := ec.unmarshalNID2string(ctx, v) - if err != nil { - return it, err - } - it.ProjectID = data - case "eventId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("eventId")) - data, err := ec.unmarshalOID2ᚖstring(ctx, v) - if err != nil { - return it, err - } - it.EventID = data - case "challengeId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("challengeId")) - data, err := ec.unmarshalOID2ᚖstring(ctx, v) - if err != nil { - return it, err - } - it.ChallengeID = data - case "points": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("points")) - data, err := ec.unmarshalNInt2int(ctx, v) - if err != nil { - return it, err - } - it.Points = data - case "hidden": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("hidden")) - data, err := ec.unmarshalNBoolean2bool(ctx, v) - if err != nil { - return it, err - } - it.Hidden = data - case "awardableFrom": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("awardableFrom")) - data, err := ec.unmarshalODateTime2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋscalarsᚐDateTime(ctx, v) + it.Description = data + case "startDate": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("startDate")) + data, err := ec.unmarshalNDateTime2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋscalarsᚐDateTime(ctx, v) if err != nil { return it, err } - it.AwardableFrom = data - case "items": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("items")) - data, err := ec.unmarshalNContentItemInput2ᚕgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐContentItemInputᚄ(ctx, v) + it.StartDate = data + case "endDate": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("endDate")) + data, err := ec.unmarshalNDateTime2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋscalarsᚐDateTime(ctx, v) if err != nil { return it, err } - it.Items = data + it.EndDate = data } } return it, nil } -func (ec *executionContext) unmarshalInputCreateEventInput(ctx context.Context, obj any) (model.CreateEventInput, error) { - var it model.CreateEventInput +func (ec *executionContext) unmarshalInputCreateLeaderboardConfigInput(ctx context.Context, obj any) (model.CreateLeaderboardConfigInput, error) { + var it model.CreateLeaderboardConfigInput asMap := map[string]any{} for k, v := range obj.(map[string]any) { asMap[k] = v } - fieldsInOrder := [...]string{"name", "description", "startDate", "endDate"} + fieldsInOrder := [...]string{"projectId", "eventId", "name", "slug", "entityType", "filter", "sortOrder", "isActive"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { + case "projectId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) + data, err := ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + it.ProjectID = data + case "eventId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("eventId")) + data, err := ec.unmarshalOID2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.EventID = data case "name": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) data, err := ec.unmarshalNString2string(ctx, v) @@ -55939,27 +57648,41 @@ func (ec *executionContext) unmarshalInputCreateEventInput(ctx context.Context, return it, err } it.Name = data - case "description": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description")) + case "slug": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("slug")) data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Description = data - case "startDate": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("startDate")) - data, err := ec.unmarshalNDateTime2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋscalarsᚐDateTime(ctx, v) + it.Slug = data + case "entityType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("entityType")) + data, err := ec.unmarshalNLeaderboardEntityType2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardEntityType(ctx, v) if err != nil { return it, err } - it.StartDate = data - case "endDate": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("endDate")) - data, err := ec.unmarshalNDateTime2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋscalarsᚐDateTime(ctx, v) + it.EntityType = data + case "filter": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filter")) + data, err := ec.unmarshalOLeaderboardFilter2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardFilter(ctx, v) if err != nil { return it, err } - it.EndDate = data + it.Filter = data + case "sortOrder": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sortOrder")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.SortOrder = data + case "isActive": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("isActive")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.IsActive = data } } @@ -57464,6 +59187,54 @@ func (ec *executionContext) unmarshalInputGrantQuizSessionAccessInput(ctx contex return it, nil } +func (ec *executionContext) unmarshalInputLeaderboardConfigFilter(ctx context.Context, obj any) (model.LeaderboardConfigFilter, error) { + var it model.LeaderboardConfigFilter + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"projectId", "eventId", "isActive", "ids"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "projectId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) + data, err := ec.unmarshalOID2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.ProjectID = data + case "eventId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("eventId")) + data, err := ec.unmarshalOID2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.EventID = data + case "isActive": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("isActive")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.IsActive = data + case "ids": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ids")) + data, err := ec.unmarshalOID2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.Ids = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputLeaderboardFilter(ctx context.Context, obj any) (model.LeaderboardFilter, error) { var it model.LeaderboardFilter asMap := map[string]any{} @@ -58643,6 +60414,75 @@ func (ec *executionContext) unmarshalInputUpdateEventInput(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputUpdateLeaderboardConfigInput(ctx context.Context, obj any) (model.UpdateLeaderboardConfigInput, error) { + var it model.UpdateLeaderboardConfigInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"name", "slug", "entityType", "filter", "clearFilter", "sortOrder", "isActive"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "slug": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("slug")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Slug = data + case "entityType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("entityType")) + data, err := ec.unmarshalOLeaderboardEntityType2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardEntityType(ctx, v) + if err != nil { + return it, err + } + it.EntityType = data + case "filter": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filter")) + data, err := ec.unmarshalOLeaderboardFilter2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardFilter(ctx, v) + if err != nil { + return it, err + } + it.Filter = data + case "clearFilter": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("clearFilter")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.ClearFilter = data + case "sortOrder": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sortOrder")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.SortOrder = data + case "isActive": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("isActive")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.IsActive = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputUpdateProjectInput(ctx context.Context, obj any) (model.UpdateProjectInput, error) { var it model.UpdateProjectInput asMap := map[string]any{} @@ -61666,61 +63506,97 @@ func (ec *executionContext) _ContentItem(ctx context.Context, sel ast.SelectionS } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "sortOrder": - out.Values[i] = ec._ContentItem_sortOrder(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.deferred, int32(len(deferred))) - - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } - - return out -} - -var eventImplementors = []string{"Event", "ScoreSource"} - -func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, obj *model.Event) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, eventImplementors) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("Event") - case "id": - out.Values[i] = ec._Event_id(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "name": - out.Values[i] = ec._Event_name(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "description": - out.Values[i] = ec._Event_description(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "challenges": + case "sortOrder": + out.Values[i] = ec._ContentItem_sortOrder(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var eventImplementors = []string{"Event", "ScoreSource"} + +func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, obj *model.Event) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, eventImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Event") + case "id": + out.Values[i] = ec._Event_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "name": + out.Values[i] = ec._Event_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "description": + out.Values[i] = ec._Event_description(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "challenges": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Event_challenges(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "leaderboard": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -61729,7 +63605,7 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Event_challenges(ctx, field, obj) + res = ec._Event_leaderboard(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -61756,7 +63632,17 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "leaderboard": + case "startDate": + out.Values[i] = ec._Event_startDate(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "endDate": + out.Values[i] = ec._Event_endDate(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "parentProject": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -61765,7 +63651,7 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Event_leaderboard(ctx, field, obj) + res = ec._Event_parentProject(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -61792,17 +63678,7 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "startDate": - out.Values[i] = ec._Event_startDate(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "endDate": - out.Values[i] = ec._Event_endDate(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "parentProject": + case "translationStatus": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -61811,7 +63687,7 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Event_parentProject(ctx, field, obj) + res = ec._Event_translationStatus(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -61838,7 +63714,7 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "translationStatus": + case "leaderboards": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -61847,7 +63723,7 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Event_translationStatus(ctx, field, obj) + res = ec._Event_leaderboards(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -63219,46 +65095,198 @@ func (ec *executionContext) _JsonQuestion(ctx context.Context, sel ast.Selection } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "questionText": - out.Values[i] = ec._JsonQuestion_questionText(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "questionOrder": - out.Values[i] = ec._JsonQuestion_questionOrder(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "timeoutSeconds": - out.Values[i] = ec._JsonQuestion_timeoutSeconds(ctx, field, obj) - case "points": - out.Values[i] = ec._JsonQuestion_points(ctx, field, obj) - case "bettingEnabled": - out.Values[i] = ec._JsonQuestion_bettingEnabled(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "bettingMinPercentage": - out.Values[i] = ec._JsonQuestion_bettingMinPercentage(ctx, field, obj) - case "bettingMaxPercentage": - out.Values[i] = ec._JsonQuestion_bettingMaxPercentage(ctx, field, obj) - case "bettingMinAbsolute": - out.Values[i] = ec._JsonQuestion_bettingMinAbsolute(ctx, field, obj) - case "bettingMaxAbsolute": - out.Values[i] = ec._JsonQuestion_bettingMaxAbsolute(ctx, field, obj) - case "translationStatus": + case "questionText": + out.Values[i] = ec._JsonQuestion_questionText(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "questionOrder": + out.Values[i] = ec._JsonQuestion_questionOrder(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "timeoutSeconds": + out.Values[i] = ec._JsonQuestion_timeoutSeconds(ctx, field, obj) + case "points": + out.Values[i] = ec._JsonQuestion_points(ctx, field, obj) + case "bettingEnabled": + out.Values[i] = ec._JsonQuestion_bettingEnabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "bettingMinPercentage": + out.Values[i] = ec._JsonQuestion_bettingMinPercentage(ctx, field, obj) + case "bettingMaxPercentage": + out.Values[i] = ec._JsonQuestion_bettingMaxPercentage(ctx, field, obj) + case "bettingMinAbsolute": + out.Values[i] = ec._JsonQuestion_bettingMinAbsolute(ctx, field, obj) + case "bettingMaxAbsolute": + out.Values[i] = ec._JsonQuestion_bettingMaxAbsolute(ctx, field, obj) + case "translationStatus": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._JsonQuestion_translationStatus(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var jsonResponseImplementors = []string{"JsonResponse", "QuizResponse"} + +func (ec *executionContext) _JsonResponse(ctx context.Context, sel ast.SelectionSet, obj *model.JSONResponse) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, jsonResponseImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("JsonResponse") + case "id": + out.Values[i] = ec._JsonResponse_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "submission": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._JsonResponse_submission(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "question": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._JsonResponse_question(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "answeredAt": + out.Values[i] = ec._JsonResponse_answeredAt(ctx, field, obj) + case "timeSpentSeconds": + out.Values[i] = ec._JsonResponse_timeSpentSeconds(ctx, field, obj) + case "pointsEarned": + out.Values[i] = ec._JsonResponse_pointsEarned(ctx, field, obj) + case "betAmount": + out.Values[i] = ec._JsonResponse_betAmount(ctx, field, obj) + case "journalEntry": field := field - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._JsonQuestion_translationStatus(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } + res = ec._JsonResponse_journalEntry(ctx, field, obj) return res } @@ -63282,6 +65310,11 @@ func (ec *executionContext) _JsonQuestion(ctx context.Context, sel ast.Selection } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "jsonResponse": + out.Values[i] = ec._JsonResponse_jsonResponse(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -63305,23 +65338,23 @@ func (ec *executionContext) _JsonQuestion(ctx context.Context, sel ast.Selection return out } -var jsonResponseImplementors = []string{"JsonResponse", "QuizResponse"} +var leaderboardConfigImplementors = []string{"LeaderboardConfig"} -func (ec *executionContext) _JsonResponse(ctx context.Context, sel ast.SelectionSet, obj *model.JSONResponse) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, jsonResponseImplementors) +func (ec *executionContext) _LeaderboardConfig(ctx context.Context, sel ast.SelectionSet, obj *model.LeaderboardConfig) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, leaderboardConfigImplementors) out := graphql.NewFieldSet(fields) deferred := make(map[string]*graphql.FieldSet) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("JsonResponse") + out.Values[i] = graphql.MarshalString("LeaderboardConfig") case "id": - out.Values[i] = ec._JsonResponse_id(ctx, field, obj) + out.Values[i] = ec._LeaderboardConfig_id(ctx, field, obj) if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } - case "submission": + case "project": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -63330,7 +65363,7 @@ func (ec *executionContext) _JsonResponse(ctx context.Context, sel ast.Selection ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._JsonResponse_submission(ctx, field, obj) + res = ec._LeaderboardConfig_project(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -63357,19 +65390,16 @@ func (ec *executionContext) _JsonResponse(ctx context.Context, sel ast.Selection } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "question": + case "event": field := field - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._JsonResponse_question(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } + res = ec._LeaderboardConfig_event(ctx, field, obj) return res } @@ -63393,24 +65423,56 @@ func (ec *executionContext) _JsonResponse(ctx context.Context, sel ast.Selection } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "answeredAt": - out.Values[i] = ec._JsonResponse_answeredAt(ctx, field, obj) - case "timeSpentSeconds": - out.Values[i] = ec._JsonResponse_timeSpentSeconds(ctx, field, obj) - case "pointsEarned": - out.Values[i] = ec._JsonResponse_pointsEarned(ctx, field, obj) - case "betAmount": - out.Values[i] = ec._JsonResponse_betAmount(ctx, field, obj) - case "journalEntry": + case "name": + out.Values[i] = ec._LeaderboardConfig_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "slug": + out.Values[i] = ec._LeaderboardConfig_slug(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "entityType": + out.Values[i] = ec._LeaderboardConfig_entityType(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "filter": + out.Values[i] = ec._LeaderboardConfig_filter(ctx, field, obj) + case "sortOrder": + out.Values[i] = ec._LeaderboardConfig_sortOrder(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "isActive": + out.Values[i] = ec._LeaderboardConfig_isActive(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "createdAt": + out.Values[i] = ec._LeaderboardConfig_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "updatedAt": + out.Values[i] = ec._LeaderboardConfig_updatedAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "leaderboard": field := field - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._JsonResponse_journalEntry(ctx, field, obj) + res = ec._LeaderboardConfig_leaderboard(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } return res } @@ -63434,10 +65496,98 @@ func (ec *executionContext) _JsonResponse(ctx context.Context, sel ast.Selection } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "jsonResponse": - out.Values[i] = ec._JsonResponse_jsonResponse(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var leaderboardConfigConnectionImplementors = []string{"LeaderboardConfigConnection"} + +func (ec *executionContext) _LeaderboardConfigConnection(ctx context.Context, sel ast.SelectionSet, obj *model.LeaderboardConfigConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, leaderboardConfigConnectionImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("LeaderboardConfigConnection") + case "edges": + out.Values[i] = ec._LeaderboardConfigConnection_edges(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ + } + case "pageInfo": + out.Values[i] = ec._LeaderboardConfigConnection_pageInfo(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "totalCount": + out.Values[i] = ec._LeaderboardConfigConnection_totalCount(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var leaderboardConfigEdgeImplementors = []string{"LeaderboardConfigEdge"} + +func (ec *executionContext) _LeaderboardConfigEdge(ctx context.Context, sel ast.SelectionSet, obj *model.LeaderboardConfigEdge) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, leaderboardConfigEdgeImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("LeaderboardConfigEdge") + case "cursor": + out.Values[i] = ec._LeaderboardConfigEdge_cursor(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "node": + out.Values[i] = ec._LeaderboardConfigEdge_node(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } default: panic("unknown field " + strconv.Quote(field.Name)) @@ -64544,6 +66694,27 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "createLeaderboardConfig": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createLeaderboardConfig(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updateLeaderboardConfig": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateLeaderboardConfig(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deleteLeaderboardConfig": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteLeaderboardConfig(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "updateAvatar": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_updateAvatar(ctx, field) @@ -66634,7 +68805,94 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "leaderboard": + case "leaderboard": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Project_leaderboard(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "events": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Project_events(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "startDate": + out.Values[i] = ec._Project_startDate(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "endDate": + out.Values[i] = ec._Project_endDate(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "branding": + out.Values[i] = ec._Project_branding(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "teams": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -66643,7 +68901,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Project_leaderboard(ctx, field, obj) + res = ec._Project_teams(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -66670,7 +68928,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "events": + case "myChurchTeams": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -66679,7 +68937,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Project_events(ctx, field, obj) + res = ec._Project_myChurchTeams(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -66706,34 +68964,16 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "startDate": - out.Values[i] = ec._Project_startDate(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "endDate": - out.Values[i] = ec._Project_endDate(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "branding": - out.Values[i] = ec._Project_branding(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "teams": + case "myTeam": field := field - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Project_teams(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } + res = ec._Project_myTeam(ctx, field, obj) return res } @@ -66757,7 +68997,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "myChurchTeams": + case "achievements": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -66766,7 +69006,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Project_myChurchTeams(ctx, field, obj) + res = ec._Project_achievements(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -66793,40 +69033,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "myTeam": - field := field - - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Project_myTeam(ctx, field, obj) - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "achievements": + case "journal": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -66835,7 +69042,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Project_achievements(ctx, field, obj) + res = ec._Project_journal(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -66862,7 +69069,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "journal": + case "myPoints": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -66871,7 +69078,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Project_journal(ctx, field, obj) + res = ec._Project_myPoints(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -66898,7 +69105,9 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "myPoints": + case "archivedAt": + out.Values[i] = ec._Project_archivedAt(ctx, field, obj) + case "translationStatus": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -66907,7 +69116,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Project_myPoints(ctx, field, obj) + res = ec._Project_translationStatus(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -66934,9 +69143,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "archivedAt": - out.Values[i] = ec._Project_archivedAt(ctx, field, obj) - case "translationStatus": + case "leaderboards": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -66945,7 +69152,7 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Project_translationStatus(ctx, field, obj) + res = ec._Project_leaderboards(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -67680,6 +69887,50 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "leaderboardConfig": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_leaderboardConfig(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "leaderboardConfigs": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_leaderboardConfigs(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "user": field := field @@ -75891,6 +78142,11 @@ func (ec *executionContext) unmarshalNCreateEventInput2githubᚗcomᚋbccᚑmedi return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNCreateLeaderboardConfigInput2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐCreateLeaderboardConfigInput(ctx context.Context, v any) (model.CreateLeaderboardConfigInput, error) { + res, err := ec.unmarshalInputCreateLeaderboardConfigInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNCreateOrderingItemInput2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐCreateOrderingItemInput(ctx context.Context, v any) (model.CreateOrderingItemInput, error) { res, err := ec.unmarshalInputCreateOrderingItemInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -76464,6 +78720,126 @@ func (ec *executionContext) marshalNJSON2string(ctx context.Context, sel ast.Sel return res } +func (ec *executionContext) marshalNLeaderboardConfig2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfig(ctx context.Context, sel ast.SelectionSet, v model.LeaderboardConfig) graphql.Marshaler { + return ec._LeaderboardConfig(ctx, sel, &v) +} + +func (ec *executionContext) marshalNLeaderboardConfig2ᚕgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigᚄ(ctx context.Context, sel ast.SelectionSet, v []model.LeaderboardConfig) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNLeaderboardConfig2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfig(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNLeaderboardConfig2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfig(ctx context.Context, sel ast.SelectionSet, v *model.LeaderboardConfig) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._LeaderboardConfig(ctx, sel, v) +} + +func (ec *executionContext) marshalNLeaderboardConfigConnection2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigConnection(ctx context.Context, sel ast.SelectionSet, v model.LeaderboardConfigConnection) graphql.Marshaler { + return ec._LeaderboardConfigConnection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNLeaderboardConfigConnection2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigConnection(ctx context.Context, sel ast.SelectionSet, v *model.LeaderboardConfigConnection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._LeaderboardConfigConnection(ctx, sel, v) +} + +func (ec *executionContext) marshalNLeaderboardConfigEdge2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigEdge(ctx context.Context, sel ast.SelectionSet, v model.LeaderboardConfigEdge) graphql.Marshaler { + return ec._LeaderboardConfigEdge(ctx, sel, &v) +} + +func (ec *executionContext) marshalNLeaderboardConfigEdge2ᚕgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []model.LeaderboardConfigEdge) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNLeaderboardConfigEdge2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigEdge(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalNLeaderboardConnection2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConnection(ctx context.Context, sel ast.SelectionSet, v model.LeaderboardConnection) graphql.Marshaler { return ec._LeaderboardConnection(ctx, sel, &v) } @@ -78327,6 +80703,11 @@ func (ec *executionContext) unmarshalNUpdateEventInput2githubᚗcomᚋbccᚑmedi return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNUpdateLeaderboardConfigInput2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐUpdateLeaderboardConfigInput(ctx context.Context, v any) (model.UpdateLeaderboardConfigInput, error) { + res, err := ec.unmarshalInputUpdateLeaderboardConfigInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNUpdateProjectInput2githubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐUpdateProjectInput(ctx context.Context, v any) (model.UpdateProjectInput, error) { res, err := ec.unmarshalInputUpdateProjectInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -79560,6 +81941,30 @@ func (ec *executionContext) marshalOJSON2ᚖstring(ctx context.Context, sel ast. return res } +func (ec *executionContext) unmarshalOLeaderboardConfigFilter2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardConfigFilter(ctx context.Context, v any) (*model.LeaderboardConfigFilter, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputLeaderboardConfigFilter(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalOLeaderboardEntityType2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardEntityType(ctx context.Context, v any) (*model.LeaderboardEntityType, error) { + if v == nil { + return nil, nil + } + var res = new(model.LeaderboardEntityType) + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOLeaderboardEntityType2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardEntityType(ctx context.Context, sel ast.SelectionSet, v *model.LeaderboardEntityType) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return v +} + func (ec *executionContext) marshalOLeaderboardEntry2ᚖgithubᚗcomᚋbccᚑmediaᚋwayfarerᚋinternalᚋgraphᚋapiᚋmodelᚐLeaderboardEntry(ctx context.Context, sel ast.SelectionSet, v *model.LeaderboardEntry) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/backend/internal/graph/api/model/models_gen.go b/backend/internal/graph/api/model/models_gen.go index 1370dca2..94ad910c 100644 --- a/backend/internal/graph/api/model/models_gen.go +++ b/backend/internal/graph/api/model/models_gen.go @@ -485,6 +485,17 @@ type CreateEventInput struct { EndDate scalars.DateTime `json:"endDate"` } +type CreateLeaderboardConfigInput struct { + ProjectID string `json:"projectId"` + EventID *string `json:"eventId,omitempty"` + Name string `json:"name"` + Slug string `json:"slug"` + EntityType LeaderboardEntityType `json:"entityType"` + Filter *LeaderboardFilter `json:"filter,omitempty"` + SortOrder *int `json:"sortOrder,omitempty"` + IsActive *bool `json:"isActive,omitempty"` +} + type CreateOrderingItemInput struct { ItemText string `json:"itemText"` CorrectOrder int `json:"correctOrder"` @@ -676,7 +687,9 @@ type Event struct { EndDate scalars.DateTime `json:"endDate"` ParentProject *Project `json:"parentProject"` TranslationStatus []TranslationFieldStatus `json:"translationStatus"` - ProjectID string `json:"-"` + // Active leaderboard configs for this event (all configs, including inactive, for admins/superadmins). + Leaderboards []LeaderboardConfig `json:"leaderboards"` + ProjectID string `json:"-"` } func (Event) IsScoreSource() {} @@ -988,6 +1001,43 @@ func (this JSONResponse) GetPointsEarned() *int { return this.PointsE func (this JSONResponse) GetBetAmount() *int { return this.BetAmount } func (this JSONResponse) GetJournalEntry() *ScoreJournal { return this.JournalEntry } +type LeaderboardConfig struct { + ID string `json:"id"` + Project *Project `json:"project"` + Event *Event `json:"event,omitempty"` + Name string `json:"name"` + Slug string `json:"slug"` + EntityType LeaderboardEntityType `json:"entityType"` + // The filter applied to this leaderboard, mirroring the `LeaderboardFilter` input shape. + Filter *string `json:"filter,omitempty"` + SortOrder int `json:"sortOrder"` + IsActive bool `json:"isActive"` + CreatedAt scalars.DateTime `json:"createdAt"` + UpdatedAt scalars.DateTime `json:"updatedAt"` + // The finished, computed leaderboard for this config. + Leaderboard *LeaderboardConnection `json:"leaderboard"` + EventID *string `json:"-"` + ProjectID string `json:"-"` +} + +type LeaderboardConfigConnection struct { + Edges []LeaderboardConfigEdge `json:"edges"` + PageInfo *PageInfo `json:"pageInfo"` + TotalCount int `json:"totalCount"` +} + +type LeaderboardConfigEdge struct { + Cursor string `json:"cursor"` + Node *LeaderboardConfig `json:"node"` +} + +type LeaderboardConfigFilter struct { + ProjectID *string `json:"projectId,omitempty"` + EventID *string `json:"eventId,omitempty"` + IsActive *bool `json:"isActive,omitempty"` + Ids []string `json:"ids,omitempty"` +} + type LeaderboardConnection struct { Edges []LeaderboardEdge `json:"edges"` PageInfo *PageInfo `json:"pageInfo"` @@ -1355,8 +1405,10 @@ type Project struct { MyPoints int `json:"myPoints"` ArchivedAt *bool `json:"archivedAt,omitempty"` TranslationStatus []TranslationFieldStatus `json:"translationStatus"` - InfoMessageRaw *string `json:"-"` - RulesRaw *string `json:"-"` + // Active leaderboard configs for this project (all configs, including inactive, for admins/superadmins). + Leaderboards []LeaderboardConfig `json:"leaderboards"` + InfoMessageRaw *string `json:"-"` + RulesRaw *string `json:"-"` } type ProjectConnection struct { @@ -2066,6 +2118,18 @@ type UpdateEventInput struct { EndDate *scalars.DateTime `json:"endDate,omitempty"` } +type UpdateLeaderboardConfigInput struct { + Name *string `json:"name,omitempty"` + Slug *string `json:"slug,omitempty"` + EntityType *LeaderboardEntityType `json:"entityType,omitempty"` + Filter *LeaderboardFilter `json:"filter,omitempty"` + // Set to true to remove the existing filter entirely (show an unfiltered leaderboard). + // Ignored if `filter` is also provided. Has no effect otherwise. + ClearFilter *bool `json:"clearFilter,omitempty"` + SortOrder *int `json:"sortOrder,omitempty"` + IsActive *bool `json:"isActive,omitempty"` +} + type UpdateProjectInput struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` diff --git a/gql/events.graphqls b/gql/events.graphqls index 1d30b2f6..bf65024d 100644 --- a/gql/events.graphqls +++ b/gql/events.graphqls @@ -14,7 +14,7 @@ type Event { after: String last: Int before: String - ): LeaderboardConnection! @goField(forceResolver: true) + ): LeaderboardConnection! @goField(forceResolver: true) @deprecated(reason: "Use `leaderboards` (LeaderboardConfig) for persisted, admin-managed leaderboards instead.") startDate: DateTime! endDate: DateTime! parentProject: Project! @goField(forceResolver: true) diff --git a/gql/leaderboards.graphqls b/gql/leaderboards.graphqls new file mode 100644 index 00000000..3f46526a --- /dev/null +++ b/gql/leaderboards.graphqls @@ -0,0 +1,101 @@ +# Persisted, admin-managed leaderboard definitions + +# ==================== LeaderboardConfig Type ==================== + +type LeaderboardConfig { + id: ID! + project: Project! @goField(forceResolver: true) + event: Event @goField(forceResolver: true) + name: String! + slug: String! + entityType: LeaderboardEntityType! + """ + The filter applied to this leaderboard, mirroring the `LeaderboardFilter` input shape. + """ + filter: JSON + sortOrder: Int! + isActive: Boolean! + createdAt: DateTime! + updatedAt: DateTime! + """ + The finished, computed leaderboard for this config. + """ + leaderboard(first: Int, after: String, last: Int, before: String): LeaderboardConnection! @goField(forceResolver: true) +} + +# ==================== Input Types ==================== + +input CreateLeaderboardConfigInput { + projectId: ID! + eventId: ID + name: String! + slug: String! + entityType: LeaderboardEntityType! + filter: LeaderboardFilter + sortOrder: Int + isActive: Boolean +} + +input UpdateLeaderboardConfigInput { + name: String + slug: String + entityType: LeaderboardEntityType + filter: LeaderboardFilter + """ + Set to true to remove the existing filter entirely (show an unfiltered leaderboard). + Ignored if `filter` is also provided. Has no effect otherwise. + """ + clearFilter: Boolean + sortOrder: Int + isActive: Boolean +} + +input LeaderboardConfigFilter { + projectId: ID + eventId: ID + isActive: Boolean + ids: [ID!] +} + +# ==================== Pagination ==================== + +type LeaderboardConfigEdge { + cursor: String! + node: LeaderboardConfig! +} + +type LeaderboardConfigConnection { + edges: [LeaderboardConfigEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +# ==================== Queries ==================== + +extend type Query { + # Admin management — includes inactive/draft configs. + leaderboardConfig(id: ID!): LeaderboardConfig! @requireRole(roles: ["admin", "superadmin"]) + leaderboardConfigs(filter: LeaderboardConfigFilter, first: Int, after: String, last: Int, before: String): LeaderboardConfigConnection! @requireRole(roles: ["admin", "superadmin"]) +} + +extend type Project { + """ + Active leaderboard configs for this project (all configs, including inactive, for admins/superadmins). + """ + leaderboards: [LeaderboardConfig!]! @goField(forceResolver: true) +} + +extend type Event { + """ + Active leaderboard configs for this event (all configs, including inactive, for admins/superadmins). + """ + leaderboards: [LeaderboardConfig!]! @goField(forceResolver: true) +} + +# ==================== Mutations ==================== + +extend type Mutation { + createLeaderboardConfig(input: CreateLeaderboardConfigInput!): LeaderboardConfig! @requireRole(roles: ["admin", "superadmin"]) + updateLeaderboardConfig(id: ID!, input: UpdateLeaderboardConfigInput!): LeaderboardConfig! @requireRole(roles: ["admin", "superadmin"]) + deleteLeaderboardConfig(id: ID!): Boolean! @requireRole(roles: ["admin", "superadmin"]) +} diff --git a/gql/projects.graphqls b/gql/projects.graphqls index fd03f7cc..ffd232c1 100644 --- a/gql/projects.graphqls +++ b/gql/projects.graphqls @@ -21,7 +21,7 @@ type Project { after: String last: Int before: String - ): LeaderboardConnection! @goField(forceResolver: true) + ): LeaderboardConnection! @goField(forceResolver: true) @deprecated(reason: "Use `leaderboards` (LeaderboardConfig) for persisted, admin-managed leaderboards instead.") events: [Event!]! @goField(forceResolver: true) startDate: DateTime! endDate: DateTime! From 24ec31c28ce570746f823610fa1c5c255a3a0505 Mon Sep 17 00:00:00 2001 From: JWSametz Date: Thu, 10 Sep 2026 14:05:43 +0200 Subject: [PATCH 4/9] feat(cache): support leaderboard config invalidation --- backend/internal/cache/invalidation.go | 21 +++++++++++++++++++++ backend/internal/cache/keys.go | 16 ++++++++++++++++ backend/internal/cache/sync.go | 25 ++++++++++++++++--------- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/backend/internal/cache/invalidation.go b/backend/internal/cache/invalidation.go index 91a57e77..e42a64fb 100644 --- a/backend/internal/cache/invalidation.go +++ b/backend/internal/cache/invalidation.go @@ -608,6 +608,27 @@ func (c *CacheWithRegistry) invalidateChallengeLocal(challengeID, projectID stri c.DeletePrefix(PrefixActiveChallengesCount) } +// InvalidateLeaderboardConfig invalidates all cache entries related to a leaderboard config and broadcasts to other instances +func (c *CacheWithRegistry) InvalidateLeaderboardConfig(configID, projectID string, eventID *string) { + c.invalidateLeaderboardConfigLocal(configID, projectID, eventID) + msg := InvalidationMessage{Type: InvalidationTypeLeaderboardConfig, ID: configID, ProjectID: projectID} + if eventID != nil { + msg.EventID = *eventID + } + c.broadcast(msg) +} + +// invalidateLeaderboardConfigLocal invalidates leaderboard config cache entries on this instance only +func (c *CacheWithRegistry) invalidateLeaderboardConfigLocal(configID, projectID string, eventID *string) { + c.Delete(LeaderboardConfigKey(configID)) + + // Invalidate config list caches for project and event + c.Delete(LeaderboardConfigsByProjectKey(projectID)) + if eventID != nil { + c.Delete(LeaderboardConfigsByEventKey(*eventID)) + } +} + // InvalidateAchievement invalidates all cache entries related to an achievement and broadcasts to other instances func (c *CacheWithRegistry) InvalidateAchievement(achievementID string) { c.invalidateAchievementLocal(achievementID) diff --git a/backend/internal/cache/keys.go b/backend/internal/cache/keys.go index 117f53b2..1de61ea4 100644 --- a/backend/internal/cache/keys.go +++ b/backend/internal/cache/keys.go @@ -27,6 +27,7 @@ const ( PrefixTeam = "team:" PrefixSuperTeam = "superteam:" PrefixChallenge = "challenge:" + PrefixLeaderboardConfig = "leaderboardconfig:" PrefixAchievement = "achievement:" PrefixUserStreakProgress = "userstreakprogress:" PrefixQuiz = "quiz:" @@ -230,6 +231,21 @@ func ChallengesByEventKey(eventID string) string { return fmt.Sprintf("%s:event:%s", PrefixChallenge, eventID) } +// LeaderboardConfigKey builds a cache key for a leaderboard config by ID +func LeaderboardConfigKey(configID string) string { + return PrefixLeaderboardConfig + configID +} + +// LeaderboardConfigsByProjectKey builds a cache key for leaderboard configs in a project +func LeaderboardConfigsByProjectKey(projectID string) string { + return fmt.Sprintf("%s:project:%s", PrefixLeaderboardConfig, projectID) +} + +// LeaderboardConfigsByEventKey builds a cache key for leaderboard configs in an event +func LeaderboardConfigsByEventKey(eventID string) string { + return fmt.Sprintf("%s:event:%s", PrefixLeaderboardConfig, eventID) +} + // AchievementKey builds a cache key for an achievement by ID func AchievementKey(achievementID string) string { return PrefixAchievement + achievementID diff --git a/backend/internal/cache/sync.go b/backend/internal/cache/sync.go index 8432a986..da292b41 100644 --- a/backend/internal/cache/sync.go +++ b/backend/internal/cache/sync.go @@ -24,15 +24,16 @@ const ( type InvalidationType string const ( - InvalidationTypeUser InvalidationType = "user" - InvalidationTypeProject InvalidationType = "project" - InvalidationTypeEvent InvalidationType = "event" - InvalidationTypeTeam InvalidationType = "team" - InvalidationTypeSuperTeam InvalidationType = "superteam" - InvalidationTypeChallenge InvalidationType = "challenge" - InvalidationTypeAchievement InvalidationType = "achievement" - InvalidationTypeQuiz InvalidationType = "quiz" - InvalidationTypeClear InvalidationType = "clear" + InvalidationTypeUser InvalidationType = "user" + InvalidationTypeProject InvalidationType = "project" + InvalidationTypeEvent InvalidationType = "event" + InvalidationTypeTeam InvalidationType = "team" + InvalidationTypeSuperTeam InvalidationType = "superteam" + InvalidationTypeChallenge InvalidationType = "challenge" + InvalidationTypeAchievement InvalidationType = "achievement" + InvalidationTypeLeaderboardConfig InvalidationType = "leaderboardconfig" + InvalidationTypeQuiz InvalidationType = "quiz" + InvalidationTypeClear InvalidationType = "clear" InvalidationTypeQuizSessionAccess InvalidationType = "quizsessionaccess" InvalidationTypeQuizSession InvalidationType = "quizsession" @@ -221,6 +222,12 @@ func (s *CacheSync) applyInvalidation(msg InvalidationMessage) { s.cache.invalidateChallengeLocal(msg.ID, msg.ProjectID, eventID) case InvalidationTypeAchievement: s.cache.invalidateAchievementLocal(msg.ID) + case InvalidationTypeLeaderboardConfig: + var eventID *string + if msg.EventID != "" { + eventID = &msg.EventID + } + s.cache.invalidateLeaderboardConfigLocal(msg.ID, msg.ProjectID, eventID) case InvalidationTypeQuiz: s.cache.invalidateQuizLocal(msg.ID, msg.ChallengeID) case InvalidationTypeUserEnrollment: From 4caa880f8a9b9be9c654e27d803df3809e15e38d Mon Sep 17 00:00:00 2001 From: JWSametz Date: Thu, 10 Sep 2026 14:06:38 +0200 Subject: [PATCH 5/9] feat(leaderboards): add leaderboard config data loaders --- .../loaders/leaderboard_config_by_id.go | 90 ++++++++++++++ .../loaders/leaderboard_config_by_id_test.go | 66 ++++++++++ .../loaders/leaderboard_configs_by_event.go | 91 ++++++++++++++ .../leaderboard_configs_by_event_test.go | 104 ++++++++++++++++ .../loaders/leaderboard_configs_by_project.go | 88 ++++++++++++++ .../leaderboard_configs_by_project_test.go | 115 ++++++++++++++++++ backend/internal/loaders/loaders.go | 6 + 7 files changed, 560 insertions(+) create mode 100644 backend/internal/loaders/leaderboard_config_by_id.go create mode 100644 backend/internal/loaders/leaderboard_config_by_id_test.go create mode 100644 backend/internal/loaders/leaderboard_configs_by_event.go create mode 100644 backend/internal/loaders/leaderboard_configs_by_event_test.go create mode 100644 backend/internal/loaders/leaderboard_configs_by_project.go create mode 100644 backend/internal/loaders/leaderboard_configs_by_project_test.go diff --git a/backend/internal/loaders/leaderboard_config_by_id.go b/backend/internal/loaders/leaderboard_config_by_id.go new file mode 100644 index 00000000..a2a3bd1b --- /dev/null +++ b/backend/internal/loaders/leaderboard_config_by_id.go @@ -0,0 +1,90 @@ +package loaders + +import ( + "context" + "fmt" + + "github.com/bcc-media/wayfarer/internal/cache" + "github.com/bcc-media/wayfarer/internal/database" + "github.com/bcc-media/wayfarer/internal/database/sqlc" + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/bcc-media/wayfarer/internal/graph/scalars" + "github.com/graph-gophers/dataloader/v7" +) + +// leaderboardConfigByIDBatchFunc batches loading leaderboard configs by IDs +func leaderboardConfigByIDBatchFunc(db *database.DB, c *cache.CacheWithRegistry) func(context.Context, []string) []*dataloader.Result[*model.LeaderboardConfig] { + return func(ctx context.Context, ids []string) []*dataloader.Result[*model.LeaderboardConfig] { + configMap := make(map[string]*model.LeaderboardConfig) + missingIDs := []string{} + seen := make(map[string]bool) + + for _, id := range ids { + if seen[id] { + continue + } + seen[id] = true + + cacheKey := cache.LeaderboardConfigKey(id) + if cached, ok := c.Get(cacheKey); ok { + if config, ok := cached.(*model.LeaderboardConfig); ok { + configMap[id] = config + continue + } + } + missingIDs = append(missingIDs, id) + } + + if len(missingIDs) > 0 { + rows, err := db.Queries.GetLeaderboardConfigsByIDs(ctx, missingIDs) + if err != nil { + results := make([]*dataloader.Result[*model.LeaderboardConfig], len(ids)) + for i := range results { + results[i] = &dataloader.Result[*model.LeaderboardConfig]{Error: err} + } + return results + } + + for _, row := range rows { + config := ConvertRowToLeaderboardConfig(row) + configMap[row.ID] = config + c.Set(cache.LeaderboardConfigKey(row.ID), config) + } + } + + results := make([]*dataloader.Result[*model.LeaderboardConfig], len(ids)) + for i, id := range ids { + if config, ok := configMap[id]; ok { + results[i] = &dataloader.Result[*model.LeaderboardConfig]{Data: config} + } else { + results[i] = &dataloader.Result[*model.LeaderboardConfig]{ + Error: fmt.Errorf("leaderboard config not found: %s", id), + } + } + } + return results + } +} + +// ConvertRowToLeaderboardConfig converts a database row to the GraphQL model +func ConvertRowToLeaderboardConfig(row *sqlc.LeaderboardConfig) *model.LeaderboardConfig { + var filter *string + if len(row.Filter) > 0 { + f := string(row.Filter) + filter = &f + } + + return &model.LeaderboardConfig{ + ID: row.ID, + ProjectID: row.ProjectID, + EventID: row.EventID, + Name: row.Name, + Slug: row.Slug, + EntityType: model.LeaderboardEntityType(row.EntityType), + Filter: filter, + SortOrder: int(row.SortOrder), + IsActive: row.IsActive, + CreatedAt: scalars.DateTime{Time: row.CreatedAt.Time}, + UpdatedAt: scalars.DateTime{Time: row.UpdatedAt.Time}, + } +} diff --git a/backend/internal/loaders/leaderboard_config_by_id_test.go b/backend/internal/loaders/leaderboard_config_by_id_test.go new file mode 100644 index 00000000..7ab55445 --- /dev/null +++ b/backend/internal/loaders/leaderboard_config_by_id_test.go @@ -0,0 +1,66 @@ +package loaders + +import ( + "testing" + "time" + + "github.com/bcc-media/wayfarer/internal/database/sqlc" + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConvertRowToLeaderboardConfig(t *testing.T) { + now := time.Now().UTC().Truncate(time.Second) + eventID := "EV01ARZ3NDEKTSV4RRFFQ69G5FAV" + + row := &sqlc.LeaderboardConfig{ + ID: "LC01ARZ3NDEKTSV4RRFFQ69G5FAV", + ProjectID: "PR01ARZ3NDEKTSV4RRFFQ69G5FAV", + EventID: &eventID, + Name: "Top Churches", + Slug: "top-churches", + EntityType: "CHURCHES", + Filter: []byte(`{"minScore":5}`), + SortOrder: 2, + IsActive: true, + CreatedAt: pgtype.Timestamptz{Time: now, Valid: true}, + UpdatedAt: pgtype.Timestamptz{Time: now, Valid: true}, + } + + result := ConvertRowToLeaderboardConfig(row) + + assert.Equal(t, row.ID, result.ID) + assert.Equal(t, row.ProjectID, result.ProjectID) + require.NotNil(t, result.EventID) + assert.Equal(t, eventID, *result.EventID) + assert.Equal(t, "Top Churches", result.Name) + assert.Equal(t, "top-churches", result.Slug) + assert.Equal(t, model.LeaderboardEntityTypeChurches, result.EntityType) + require.NotNil(t, result.Filter) + assert.JSONEq(t, `{"minScore":5}`, *result.Filter) + assert.Equal(t, 2, result.SortOrder) + assert.True(t, result.IsActive) + assert.True(t, now.Equal(result.CreatedAt.Time)) +} + +func TestConvertRowToLeaderboardConfig_NilFilterAndEvent(t *testing.T) { + row := &sqlc.LeaderboardConfig{ + ID: "LC01ARZ3NDEKTSV4RRFFQ69G5FAV", + ProjectID: "PR01ARZ3NDEKTSV4RRFFQ69G5FAV", + EventID: nil, + Name: "Global", + Slug: "global", + EntityType: "PERSONS", + Filter: nil, + SortOrder: 0, + IsActive: false, + } + + result := ConvertRowToLeaderboardConfig(row) + + assert.Nil(t, result.EventID) + assert.Nil(t, result.Filter) + assert.False(t, result.IsActive) +} diff --git a/backend/internal/loaders/leaderboard_configs_by_event.go b/backend/internal/loaders/leaderboard_configs_by_event.go new file mode 100644 index 00000000..bcf2418d --- /dev/null +++ b/backend/internal/loaders/leaderboard_configs_by_event.go @@ -0,0 +1,91 @@ +package loaders + +import ( + "context" + + "github.com/bcc-media/wayfarer/internal/cache" + "github.com/bcc-media/wayfarer/internal/database" + "github.com/bcc-media/wayfarer/internal/database/sqlc" + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/graph-gophers/dataloader/v7" +) + +// leaderboardConfigsByEventBatchFunc batches loading leaderboard configs by event IDs. +// Returns ALL configs (including inactive) — non-admin visibility filtering happens in the resolver. +func leaderboardConfigsByEventBatchFunc(db *database.DB, c *cache.CacheWithRegistry) func(context.Context, []string) []*dataloader.Result[[]*model.LeaderboardConfig] { + return func(ctx context.Context, eventIDs []string) []*dataloader.Result[[]*model.LeaderboardConfig] { + configsByEvent, missingEventIDs := partitionLeaderboardConfigsByEventCache(eventIDs, c) + + if len(missingEventIDs) > 0 { + rows, err := db.Queries.GetLeaderboardConfigsByEventIDs(ctx, missingEventIDs) + if err != nil { + results := make([]*dataloader.Result[[]*model.LeaderboardConfig], len(eventIDs)) + for i := range results { + results[i] = &dataloader.Result[[]*model.LeaderboardConfig]{Error: err} + } + return results + } + storeLeaderboardConfigsByEventInCache(missingEventIDs, rows, configsByEvent, c) + } + + results := make([]*dataloader.Result[[]*model.LeaderboardConfig], len(eventIDs)) + for i, eventID := range eventIDs { + configs := configsByEvent[eventID] + if configs == nil { + configs = []*model.LeaderboardConfig{} + } + results[i] = &dataloader.Result[[]*model.LeaderboardConfig]{Data: configs} + } + return results + } +} + +// partitionLeaderboardConfigsByEventCache splits the requested (possibly duplicated) event +// IDs into those already served from cache and the deduplicated set that still needs a DB +// round-trip. +func partitionLeaderboardConfigsByEventCache(eventIDs []string, c *cache.CacheWithRegistry) (cached map[string][]*model.LeaderboardConfig, missing []string) { + cached = make(map[string][]*model.LeaderboardConfig) + missing = []string{} + seen := make(map[string]bool) + + for _, eventID := range eventIDs { + if seen[eventID] { + continue + } + seen[eventID] = true + + cacheKey := cache.LeaderboardConfigsByEventKey(eventID) + if val, ok := c.Get(cacheKey); ok { + if configs, ok := val.([]*model.LeaderboardConfig); ok { + cached[eventID] = configs + continue + } + } + missing = append(missing, eventID) + } + + return cached, missing +} + +// storeLeaderboardConfigsByEventInCache groups freshly fetched rows by event ID and writes +// each requested-but-missing event's (possibly empty) config list into both the result map +// and the cache, so an event with zero configs is cached as an empty slice rather than +// remaining a permanent cache miss on subsequent loads. +func storeLeaderboardConfigsByEventInCache(missingEventIDs []string, rows []*sqlc.LeaderboardConfig, configsByEvent map[string][]*model.LeaderboardConfig, c *cache.CacheWithRegistry) { + for _, row := range rows { + // row.EventID is guaranteed non-nil: the query filters on + // event_id = ANY(@event_ids), and NULL = ANY(non-null array) is + // never true in Postgres, so NULL event_id rows are excluded. + config := ConvertRowToLeaderboardConfig(row) + configsByEvent[*row.EventID] = append(configsByEvent[*row.EventID], config) + } + + for _, eventID := range missingEventIDs { + configs := configsByEvent[eventID] + if configs == nil { + configs = []*model.LeaderboardConfig{} + } + configsByEvent[eventID] = configs + c.Set(cache.LeaderboardConfigsByEventKey(eventID), configs) + } +} diff --git a/backend/internal/loaders/leaderboard_configs_by_event_test.go b/backend/internal/loaders/leaderboard_configs_by_event_test.go new file mode 100644 index 00000000..50eb6cd1 --- /dev/null +++ b/backend/internal/loaders/leaderboard_configs_by_event_test.go @@ -0,0 +1,104 @@ +package loaders + +import ( + "testing" + + "github.com/bcc-media/wayfarer/internal/cache" + "github.com/bcc-media/wayfarer/internal/database/sqlc" + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPartitionLeaderboardConfigsByEventCache_DeduplicatesInput(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + cached, missing := partitionLeaderboardConfigsByEventCache( + []string{"EV001", "EV002", "EV001", "EV002", "EV001"}, c, + ) + + assert.Empty(t, cached) + assert.ElementsMatch(t, []string{"EV001", "EV002"}, missing) + assert.Len(t, missing, 2, "each event ID should appear at most once in missing") +} + +func TestPartitionLeaderboardConfigsByEventCache_MixOfCachedAndUncached(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + cachedConfig := &model.LeaderboardConfig{ID: "LC001", Name: "Cached Config"} + c.Set(cache.LeaderboardConfigsByEventKey("EV001"), []*model.LeaderboardConfig{cachedConfig}) + c.Wait() + + cached, missing := partitionLeaderboardConfigsByEventCache( + []string{"EV001", "EV002", "EV003"}, c, + ) + + require.Contains(t, cached, "EV001") + assert.Equal(t, []*model.LeaderboardConfig{cachedConfig}, cached["EV001"]) + assert.NotContains(t, cached, "EV002") + assert.NotContains(t, cached, "EV003") + assert.ElementsMatch(t, []string{"EV002", "EV003"}, missing) +} + +func TestPartitionLeaderboardConfigsByEventCache_DuplicateCachedIDOnlyLookedUpOnce(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + c.Set(cache.LeaderboardConfigsByEventKey("EV001"), []*model.LeaderboardConfig{}) + c.Wait() + + hitsBefore := c.Hits() + + cached, missing := partitionLeaderboardConfigsByEventCache( + []string{"EV001", "EV001", "EV001"}, c, + ) + + hitsAfter := c.Hits() + + assert.Contains(t, cached, "EV001") + assert.Empty(t, missing) + assert.Equal(t, uint64(1), hitsAfter-hitsBefore, "a duplicate already-cached ID should only be looked up once") +} + +func TestStoreLeaderboardConfigsByEventInCache_EmptyResultIsCached(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + configsByEvent := make(map[string][]*model.LeaderboardConfig) + storeLeaderboardConfigsByEventInCache([]string{"EV001"}, []*sqlc.LeaderboardConfig{}, configsByEvent, c) + c.Wait() + + require.Contains(t, configsByEvent, "EV001") + assert.Empty(t, configsByEvent["EV001"]) + assert.NotNil(t, configsByEvent["EV001"], "empty result should be an empty slice, not nil") + + cached, ok := c.Get(cache.LeaderboardConfigsByEventKey("EV001")) + require.True(t, ok, "an empty result should still be cached to avoid re-querying") + assert.Equal(t, []*model.LeaderboardConfig{}, cached) +} + +func TestStoreLeaderboardConfigsByEventInCache_GroupsRowsByEvent(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + eventID1 := "EV001" + eventID2 := "EV002" + rows := []*sqlc.LeaderboardConfig{ + {ID: "LC001", EventID: &eventID1, Name: "A", EntityType: "PERSONS"}, + {ID: "LC002", EventID: &eventID1, Name: "B", EntityType: "TEAMS"}, + {ID: "LC003", EventID: &eventID2, Name: "C", EntityType: "CHURCHES"}, + } + + configsByEvent := make(map[string][]*model.LeaderboardConfig) + storeLeaderboardConfigsByEventInCache([]string{eventID1, eventID2}, rows, configsByEvent, c) + c.Wait() + + require.Len(t, configsByEvent[eventID1], 2) + require.Len(t, configsByEvent[eventID2], 1) + + cachedEV001, ok := c.Get(cache.LeaderboardConfigsByEventKey(eventID1)) + require.True(t, ok) + assert.Len(t, cachedEV001, 2) +} diff --git a/backend/internal/loaders/leaderboard_configs_by_project.go b/backend/internal/loaders/leaderboard_configs_by_project.go new file mode 100644 index 00000000..381fffe5 --- /dev/null +++ b/backend/internal/loaders/leaderboard_configs_by_project.go @@ -0,0 +1,88 @@ +package loaders + +import ( + "context" + + "github.com/bcc-media/wayfarer/internal/cache" + "github.com/bcc-media/wayfarer/internal/database" + "github.com/bcc-media/wayfarer/internal/database/sqlc" + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/graph-gophers/dataloader/v7" +) + +// leaderboardConfigsByProjectBatchFunc batches loading leaderboard configs by project IDs. +// Returns ALL configs (including inactive) — non-admin visibility filtering happens in the resolver. +func leaderboardConfigsByProjectBatchFunc(db *database.DB, c *cache.CacheWithRegistry) func(context.Context, []string) []*dataloader.Result[[]*model.LeaderboardConfig] { + return func(ctx context.Context, projectIDs []string) []*dataloader.Result[[]*model.LeaderboardConfig] { + configsByProject, missingProjectIDs := partitionLeaderboardConfigsByProjectCache(projectIDs, c) + + if len(missingProjectIDs) > 0 { + rows, err := db.Queries.GetLeaderboardConfigsByProjectIDs(ctx, missingProjectIDs) + if err != nil { + results := make([]*dataloader.Result[[]*model.LeaderboardConfig], len(projectIDs)) + for i := range results { + results[i] = &dataloader.Result[[]*model.LeaderboardConfig]{Error: err} + } + return results + } + storeLeaderboardConfigsByProjectInCache(missingProjectIDs, rows, configsByProject, c) + } + + results := make([]*dataloader.Result[[]*model.LeaderboardConfig], len(projectIDs)) + for i, projectID := range projectIDs { + configs := configsByProject[projectID] + if configs == nil { + configs = []*model.LeaderboardConfig{} + } + results[i] = &dataloader.Result[[]*model.LeaderboardConfig]{Data: configs} + } + return results + } +} + +// partitionLeaderboardConfigsByProjectCache splits the requested (possibly duplicated) +// project IDs into those already served from cache and the deduplicated set that still +// needs a DB round-trip. +func partitionLeaderboardConfigsByProjectCache(projectIDs []string, c *cache.CacheWithRegistry) (cached map[string][]*model.LeaderboardConfig, missing []string) { + cached = make(map[string][]*model.LeaderboardConfig) + missing = []string{} + seen := make(map[string]bool) + + for _, projectID := range projectIDs { + if seen[projectID] { + continue + } + seen[projectID] = true + + cacheKey := cache.LeaderboardConfigsByProjectKey(projectID) + if val, ok := c.Get(cacheKey); ok { + if configs, ok := val.([]*model.LeaderboardConfig); ok { + cached[projectID] = configs + continue + } + } + missing = append(missing, projectID) + } + + return cached, missing +} + +// storeLeaderboardConfigsByProjectInCache groups freshly fetched rows by project ID and +// writes each requested-but-missing project's (possibly empty) config list into both the +// result map and the cache, so a project with zero configs is cached as an empty slice +// rather than remaining a permanent cache miss on subsequent loads. +func storeLeaderboardConfigsByProjectInCache(missingProjectIDs []string, rows []*sqlc.LeaderboardConfig, configsByProject map[string][]*model.LeaderboardConfig, c *cache.CacheWithRegistry) { + for _, row := range rows { + config := ConvertRowToLeaderboardConfig(row) + configsByProject[row.ProjectID] = append(configsByProject[row.ProjectID], config) + } + + for _, projectID := range missingProjectIDs { + configs := configsByProject[projectID] + if configs == nil { + configs = []*model.LeaderboardConfig{} + } + configsByProject[projectID] = configs + c.Set(cache.LeaderboardConfigsByProjectKey(projectID), configs) + } +} diff --git a/backend/internal/loaders/leaderboard_configs_by_project_test.go b/backend/internal/loaders/leaderboard_configs_by_project_test.go new file mode 100644 index 00000000..ba6cedfe --- /dev/null +++ b/backend/internal/loaders/leaderboard_configs_by_project_test.go @@ -0,0 +1,115 @@ +package loaders + +import ( + "testing" + + "github.com/bcc-media/wayfarer/internal/cache" + "github.com/bcc-media/wayfarer/internal/database/sqlc" + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPartitionLeaderboardConfigsByProjectCache_DeduplicatesInput(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + cached, missing := partitionLeaderboardConfigsByProjectCache( + []string{"PR001", "PR002", "PR001", "PR002", "PR001"}, c, + ) + + assert.Empty(t, cached) + assert.ElementsMatch(t, []string{"PR001", "PR002"}, missing) + assert.Len(t, missing, 2, "each project ID should appear at most once in missing") +} + +func TestPartitionLeaderboardConfigsByProjectCache_MixOfCachedAndUncached(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + cachedConfig := &model.LeaderboardConfig{ID: "LC001", Name: "Cached Config"} + c.Set(cache.LeaderboardConfigsByProjectKey("PR001"), []*model.LeaderboardConfig{cachedConfig}) + c.Wait() // deterministically flush ristretto's async write buffer + + cached, missing := partitionLeaderboardConfigsByProjectCache( + []string{"PR001", "PR002", "PR003"}, c, + ) + + require.Contains(t, cached, "PR001") + assert.Equal(t, []*model.LeaderboardConfig{cachedConfig}, cached["PR001"]) + assert.NotContains(t, cached, "PR002") + assert.NotContains(t, cached, "PR003") + assert.ElementsMatch(t, []string{"PR002", "PR003"}, missing) +} + +func TestPartitionLeaderboardConfigsByProjectCache_DuplicateCachedIDOnlyLookedUpOnce(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + c.Set(cache.LeaderboardConfigsByProjectKey("PR001"), []*model.LeaderboardConfig{}) + c.Wait() + + hitsBefore := c.Hits() + + cached, missing := partitionLeaderboardConfigsByProjectCache( + []string{"PR001", "PR001", "PR001"}, c, + ) + + hitsAfter := c.Hits() + + assert.Contains(t, cached, "PR001") + assert.Empty(t, missing) + assert.Equal(t, uint64(1), hitsAfter-hitsBefore, "a duplicate already-cached ID should only be looked up once") +} + +func TestPartitionLeaderboardConfigsByProjectCache_AllCached(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + c.Set(cache.LeaderboardConfigsByProjectKey("PR001"), []*model.LeaderboardConfig{}) + c.Wait() + + cached, missing := partitionLeaderboardConfigsByProjectCache([]string{"PR001"}, c) + + require.Contains(t, cached, "PR001") + assert.Empty(t, missing) +} + +func TestStoreLeaderboardConfigsByProjectInCache_EmptyResultIsCached(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + configsByProject := make(map[string][]*model.LeaderboardConfig) + storeLeaderboardConfigsByProjectInCache([]string{"PR001"}, []*sqlc.LeaderboardConfig{}, configsByProject, c) + c.Wait() + + require.Contains(t, configsByProject, "PR001") + assert.Empty(t, configsByProject["PR001"]) + assert.NotNil(t, configsByProject["PR001"], "empty result should be an empty slice, not nil") + + cached, ok := c.Get(cache.LeaderboardConfigsByProjectKey("PR001")) + require.True(t, ok, "an empty result should still be cached to avoid re-querying") + assert.Equal(t, []*model.LeaderboardConfig{}, cached) +} + +func TestStoreLeaderboardConfigsByProjectInCache_GroupsRowsByProject(t *testing.T) { + c, err := cache.NewCacheWithRegistry(cache.DefaultConfig()) + require.NoError(t, err) + + rows := []*sqlc.LeaderboardConfig{ + {ID: "LC001", ProjectID: "PR001", Name: "A", EntityType: "PERSONS"}, + {ID: "LC002", ProjectID: "PR001", Name: "B", EntityType: "TEAMS"}, + {ID: "LC003", ProjectID: "PR002", Name: "C", EntityType: "CHURCHES"}, + } + + configsByProject := make(map[string][]*model.LeaderboardConfig) + storeLeaderboardConfigsByProjectInCache([]string{"PR001", "PR002"}, rows, configsByProject, c) + c.Wait() + + require.Len(t, configsByProject["PR001"], 2) + require.Len(t, configsByProject["PR002"], 1) + + cachedPR001, ok := c.Get(cache.LeaderboardConfigsByProjectKey("PR001")) + require.True(t, ok) + assert.Len(t, cachedPR001, 2) +} diff --git a/backend/internal/loaders/loaders.go b/backend/internal/loaders/loaders.go index ffc61403..b7f74ef7 100644 --- a/backend/internal/loaders/loaders.go +++ b/backend/internal/loaders/loaders.go @@ -36,6 +36,9 @@ type Loaders struct { ChallengeByIDLoader *dataloader.Loader[string, model.Challenge] ChallengesByProjectLoader *dataloader.Loader[string, []model.Challenge] ChallengesByEventLoader *dataloader.Loader[string, []model.Challenge] + LeaderboardConfigByIDLoader *dataloader.Loader[string, *model.LeaderboardConfig] + LeaderboardConfigsByProjectLoader *dataloader.Loader[string, []*model.LeaderboardConfig] + LeaderboardConfigsByEventLoader *dataloader.Loader[string, []*model.LeaderboardConfig] StreakItemsByAchievementLoader *dataloader.Loader[string, []*model.ContentItem] UserStreakProgressLoader *dataloader.Loader[UserAchievementKey, []*sqlc.UserStreakProgress] UserContentProgressLoader *dataloader.Loader[UserAchievementKey, []*sqlc.UserContentProgress] @@ -110,6 +113,9 @@ func NewLoaders(db *database.DB, cache *cache.CacheWithRegistry) *Loaders { ChallengeByIDLoader: newBatchedLoader(challengeByIDBatchFunc(db, cache)), ChallengesByProjectLoader: newBatchedLoader(challengesByProjectBatchFunc(db, cache)), ChallengesByEventLoader: newBatchedLoader(challengesByEventBatchFunc(db, cache)), + LeaderboardConfigByIDLoader: newBatchedLoader(leaderboardConfigByIDBatchFunc(db, cache)), + LeaderboardConfigsByProjectLoader: newBatchedLoader(leaderboardConfigsByProjectBatchFunc(db, cache)), + LeaderboardConfigsByEventLoader: newBatchedLoader(leaderboardConfigsByEventBatchFunc(db, cache)), StreakItemsByAchievementLoader: newBatchedLoader(streakItemsByAchievementBatchFunc(db, cache)), UserStreakProgressLoader: newBatchedLoader(userStreakProgressBatchFunc(db, cache)), UserContentProgressLoader: newBatchedLoader(userContentProgressBatchFunc(db, cache)), From 08b2397a087c0bc1a61ae2e30a4c4821d11b8578 Mon Sep 17 00:00:00 2001 From: JWSametz Date: Thu, 10 Sep 2026 14:07:16 +0200 Subject: [PATCH 6/9] feat(leaderboards): add leaderboard config pagination --- .../internal/graph/pagination/connection.go | 68 +++++ backend/internal/graph/pagination/cursor.go | 48 ++++ .../leaderboard_config_connection_test.go | 268 ++++++++++++++++++ .../leaderboard_config_cursor_test.go | 110 +++++++ 4 files changed, 494 insertions(+) create mode 100644 backend/internal/graph/pagination/leaderboard_config_connection_test.go create mode 100644 backend/internal/graph/pagination/leaderboard_config_cursor_test.go diff --git a/backend/internal/graph/pagination/connection.go b/backend/internal/graph/pagination/connection.go index dbe492a4..0217571b 100644 --- a/backend/internal/graph/pagination/connection.go +++ b/backend/internal/graph/pagination/connection.go @@ -769,3 +769,71 @@ func buildBulkJobPageInfo(params BuildBulkJobConnectionParams, edges []model.Bul return pageInfo } + +// BuildLeaderboardConfigConnectionParams holds parameters for building a leaderboard config connection +type BuildLeaderboardConfigConnectionParams struct { + Configs []*model.LeaderboardConfig + RequestedFirst *int + RequestedLast *int + RequestedAfter *string + RequestedBefore *string + TotalCount int + HasMore bool +} + +// BuildLeaderboardConfigConnection constructs a Relay-style connection from query results. +// Unlike Challenge (an interface with several concrete types, requiring a caller-supplied +// parallel timestamp slice), LeaderboardConfig is a single concrete struct that already +// carries CreatedAt, so the cursor timestamp is read directly off each config. +func BuildLeaderboardConfigConnection(params BuildLeaderboardConfigConnectionParams) *model.LeaderboardConfigConnection { + edges := make([]model.LeaderboardConfigEdge, len(params.Configs)) + for i, config := range params.Configs { + cursor := EncodeLeaderboardConfigCursor(config.CreatedAt.Time, config.ID) + edges[i] = model.LeaderboardConfigEdge{ + Cursor: cursor, + Node: config, + } + } + + pageInfo := buildLeaderboardConfigPageInfo(params, edges) + + return &model.LeaderboardConfigConnection{ + Edges: edges, + PageInfo: pageInfo, + TotalCount: params.TotalCount, + } +} + +// buildLeaderboardConfigPageInfo constructs the PageInfo for leaderboard configs +func buildLeaderboardConfigPageInfo(params BuildLeaderboardConfigConnectionParams, edges []model.LeaderboardConfigEdge) *model.PageInfo { + pageInfo := &model.PageInfo{ + HasNextPage: false, + HasPreviousPage: false, + StartCursor: nil, + EndCursor: nil, + } + + if len(edges) == 0 { + return pageInfo + } + + startCursor := edges[0].Cursor + endCursor := edges[len(edges)-1].Cursor + pageInfo.StartCursor = &startCursor + pageInfo.EndCursor = &endCursor + + if params.RequestedFirst != nil { + pageInfo.HasNextPage = params.HasMore + } + + if params.RequestedLast != nil { + pageInfo.HasPreviousPage = params.HasMore + if params.RequestedBefore != nil && *params.RequestedBefore != "" { + pageInfo.HasNextPage = true + } + } else if params.RequestedAfter != nil && *params.RequestedAfter != "" { + pageInfo.HasPreviousPage = true + } + + return pageInfo +} diff --git a/backend/internal/graph/pagination/cursor.go b/backend/internal/graph/pagination/cursor.go index fc351940..af745212 100644 --- a/backend/internal/graph/pagination/cursor.go +++ b/backend/internal/graph/pagination/cursor.go @@ -81,3 +81,51 @@ func DecodeChallengeCursor(cursor string) (ChallengeCursor, error) { ID: parts[1], }, nil } + +// LeaderboardConfigCursor represents a decoded leaderboard config cursor with timestamp and ID +type LeaderboardConfigCursor struct { + CreatedAt time.Time + ID string +} + +// EncodeLeaderboardConfigCursor encodes a timestamp and ID into a composite cursor string +// Format: base64(RFC3339_timestamp|id) +func EncodeLeaderboardConfigCursor(createdAt time.Time, id string) string { + if id == "" { + return "" + } + raw := createdAt.Format(time.RFC3339Nano) + "|" + id + return base64.StdEncoding.EncodeToString([]byte(raw)) +} + +// DecodeLeaderboardConfigCursor decodes a composite cursor string back to timestamp and ID +func DecodeLeaderboardConfigCursor(cursor string) (LeaderboardConfigCursor, error) { + if cursor == "" { + return LeaderboardConfigCursor{}, nil + } + + decoded, err := base64.StdEncoding.DecodeString(cursor) + if err != nil { + return LeaderboardConfigCursor{}, fmt.Errorf("invalid cursor format: %w", err) + } + + raw := string(decoded) + parts := strings.SplitN(raw, "|", 2) + if len(parts) != 2 { + return LeaderboardConfigCursor{}, fmt.Errorf("invalid leaderboard config cursor format: expected timestamp|id") + } + + createdAt, err := time.Parse(time.RFC3339Nano, parts[0]) + if err != nil { + return LeaderboardConfigCursor{}, fmt.Errorf("invalid timestamp in cursor: %w", err) + } + + if parts[1] == "" { + return LeaderboardConfigCursor{}, fmt.Errorf("cursor decoded to empty ID") + } + + return LeaderboardConfigCursor{ + CreatedAt: createdAt, + ID: parts[1], + }, nil +} diff --git a/backend/internal/graph/pagination/leaderboard_config_connection_test.go b/backend/internal/graph/pagination/leaderboard_config_connection_test.go new file mode 100644 index 00000000..e791a1f4 --- /dev/null +++ b/backend/internal/graph/pagination/leaderboard_config_connection_test.go @@ -0,0 +1,268 @@ +package pagination + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/bcc-media/wayfarer/internal/graph/scalars" +) + +func leaderboardConfigWithCreatedAt(id string, createdAt time.Time) *model.LeaderboardConfig { + return &model.LeaderboardConfig{ + ID: id, + Name: "Config " + id, + CreatedAt: scalars.DateTime{Time: createdAt}, + } +} + +func TestBuildLeaderboardConfigConnection_EmptyResults(t *testing.T) { + params := BuildLeaderboardConfigConnectionParams{ + Configs: []*model.LeaderboardConfig{}, + RequestedFirst: intPtr(10), + RequestedLast: nil, + RequestedAfter: nil, + RequestedBefore: nil, + TotalCount: 0, + HasMore: false, + } + + conn := BuildLeaderboardConfigConnection(params) + + require.NotNil(t, conn) + assert.Empty(t, conn.Edges) + assert.Equal(t, 0, conn.TotalCount) + require.NotNil(t, conn.PageInfo) + assert.False(t, conn.PageInfo.HasNextPage) + assert.False(t, conn.PageInfo.HasPreviousPage) + assert.Nil(t, conn.PageInfo.StartCursor) + assert.Nil(t, conn.PageInfo.EndCursor) +} + +func TestBuildLeaderboardConfigConnection_ForwardPagination(t *testing.T) { + base := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC) + configs := []*model.LeaderboardConfig{ + leaderboardConfigWithCreatedAt("LC001", base), + leaderboardConfigWithCreatedAt("LC002", base.Add(time.Minute)), + leaderboardConfigWithCreatedAt("LC003", base.Add(2*time.Minute)), + } + + tests := []struct { + name string + params BuildLeaderboardConfigConnectionParams + expectedEdgeCount int + expectedHasNextPage bool + expectedHasPrevPage bool + }{ + { + name: "first page with more results", + params: BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedFirst: intPtr(3), + RequestedAfter: nil, + RequestedBefore: nil, + TotalCount: 10, + HasMore: true, + }, + expectedEdgeCount: 3, + expectedHasNextPage: true, + expectedHasPrevPage: false, + }, + { + name: "first page with no more results", + params: BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedFirst: intPtr(3), + RequestedAfter: nil, + RequestedBefore: nil, + TotalCount: 3, + HasMore: false, + }, + expectedEdgeCount: 3, + expectedHasNextPage: false, + expectedHasPrevPage: false, + }, + { + name: "subsequent page with after cursor and more results", + params: BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedFirst: intPtr(3), + RequestedAfter: stringPtr(EncodeLeaderboardConfigCursor(base, "LC000")), + TotalCount: 10, + HasMore: true, + }, + expectedEdgeCount: 3, + expectedHasNextPage: true, + expectedHasPrevPage: true, + }, + { + name: "last page with after cursor and no more results", + params: BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedFirst: intPtr(3), + RequestedAfter: stringPtr(EncodeLeaderboardConfigCursor(base, "LC000")), + TotalCount: 6, + HasMore: false, + }, + expectedEdgeCount: 3, + expectedHasNextPage: false, + expectedHasPrevPage: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conn := BuildLeaderboardConfigConnection(tt.params) + + require.NotNil(t, conn) + assert.Equal(t, tt.expectedEdgeCount, len(conn.Edges)) + assert.Equal(t, tt.params.TotalCount, conn.TotalCount) + + require.NotNil(t, conn.PageInfo) + assert.Equal(t, tt.expectedHasNextPage, conn.PageInfo.HasNextPage) + assert.Equal(t, tt.expectedHasPrevPage, conn.PageInfo.HasPreviousPage) + + if len(conn.Edges) > 0 { + require.NotNil(t, conn.PageInfo.StartCursor) + require.NotNil(t, conn.PageInfo.EndCursor) + assert.Equal(t, conn.Edges[0].Cursor, *conn.PageInfo.StartCursor) + assert.Equal(t, conn.Edges[len(conn.Edges)-1].Cursor, *conn.PageInfo.EndCursor) + } + }) + } +} + +func TestBuildLeaderboardConfigConnection_BackwardPagination(t *testing.T) { + base := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC) + configs := []*model.LeaderboardConfig{ + leaderboardConfigWithCreatedAt("LC001", base), + leaderboardConfigWithCreatedAt("LC002", base.Add(time.Minute)), + leaderboardConfigWithCreatedAt("LC003", base.Add(2*time.Minute)), + } + + tests := []struct { + name string + params BuildLeaderboardConfigConnectionParams + expectedEdgeCount int + expectedHasNextPage bool + expectedHasPrevPage bool + }{ + { + name: "last page with more previous results", + params: BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedLast: intPtr(3), + TotalCount: 10, + HasMore: true, + }, + expectedEdgeCount: 3, + expectedHasNextPage: false, + expectedHasPrevPage: true, + }, + { + name: "last page with no previous results", + params: BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedLast: intPtr(3), + TotalCount: 3, + HasMore: false, + }, + expectedEdgeCount: 3, + expectedHasNextPage: false, + expectedHasPrevPage: false, + }, + { + name: "previous page with before cursor", + params: BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedLast: intPtr(3), + RequestedBefore: stringPtr(EncodeLeaderboardConfigCursor(base.Add(3*time.Minute), "LC004")), + TotalCount: 10, + HasMore: true, + }, + expectedEdgeCount: 3, + expectedHasNextPage: true, + expectedHasPrevPage: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conn := BuildLeaderboardConfigConnection(tt.params) + + require.NotNil(t, conn) + assert.Equal(t, tt.expectedEdgeCount, len(conn.Edges)) + assert.Equal(t, tt.params.TotalCount, conn.TotalCount) + + require.NotNil(t, conn.PageInfo) + assert.Equal(t, tt.expectedHasNextPage, conn.PageInfo.HasNextPage) + assert.Equal(t, tt.expectedHasPrevPage, conn.PageInfo.HasPreviousPage) + + if len(conn.Edges) > 0 { + require.NotNil(t, conn.PageInfo.StartCursor) + require.NotNil(t, conn.PageInfo.EndCursor) + } + }) + } +} + +func TestBuildLeaderboardConfigConnection_EdgeContent(t *testing.T) { + base := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC) + configs := []*model.LeaderboardConfig{ + leaderboardConfigWithCreatedAt("LC001", base), + leaderboardConfigWithCreatedAt("LC002", base.Add(time.Minute)), + leaderboardConfigWithCreatedAt("LC003", base.Add(2*time.Minute)), + } + + params := BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedFirst: intPtr(3), + TotalCount: 3, + HasMore: false, + } + + conn := BuildLeaderboardConfigConnection(params) + + require.NotNil(t, conn) + require.Equal(t, 3, len(conn.Edges)) + + for i, edge := range conn.Edges { + expectedCursor := EncodeLeaderboardConfigCursor(configs[i].CreatedAt.Time, configs[i].ID) + assert.Equal(t, expectedCursor, edge.Cursor) + + require.NotNil(t, edge.Node) + assert.Equal(t, configs[i].ID, edge.Node.ID) + assert.Equal(t, configs[i].Name, edge.Node.Name) + } +} + +func TestBuildLeaderboardConfigConnection_NoPaginationParams(t *testing.T) { + base := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC) + configs := []*model.LeaderboardConfig{ + leaderboardConfigWithCreatedAt("LC001", base), + leaderboardConfigWithCreatedAt("LC002", base.Add(time.Minute)), + } + + params := BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedFirst: nil, + RequestedLast: nil, + RequestedAfter: nil, + RequestedBefore: nil, + TotalCount: 2, + HasMore: false, + } + + conn := BuildLeaderboardConfigConnection(params) + + require.NotNil(t, conn) + assert.Equal(t, 2, len(conn.Edges)) + assert.Equal(t, 2, conn.TotalCount) + + require.NotNil(t, conn.PageInfo) + assert.False(t, conn.PageInfo.HasNextPage) + assert.False(t, conn.PageInfo.HasPreviousPage) +} diff --git a/backend/internal/graph/pagination/leaderboard_config_cursor_test.go b/backend/internal/graph/pagination/leaderboard_config_cursor_test.go new file mode 100644 index 00000000..62dcc87e --- /dev/null +++ b/backend/internal/graph/pagination/leaderboard_config_cursor_test.go @@ -0,0 +1,110 @@ +package pagination + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncodeLeaderboardConfigCursor(t *testing.T) { + testTime := time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC) + + tests := []struct { + name string + createdAt time.Time + id string + wantEmpty bool + }{ + {"encode valid cursor", testTime, "LC01ARZ3NDEKTSV4RRFFQ69G5FAV", false}, + {"encode with empty ID returns empty", testTime, "", true}, + {"encode with zero time", time.Time{}, "LC001", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := EncodeLeaderboardConfigCursor(tt.createdAt, tt.id) + if tt.wantEmpty { + assert.Empty(t, result) + } else { + assert.NotEmpty(t, result) + } + }) + } +} + +func TestDecodeLeaderboardConfigCursor(t *testing.T) { + testTime := time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC) + + tests := []struct { + name string + cursor string + expectedTime time.Time + expectedID string + expectError bool + errorContains string + }{ + { + name: "decode empty cursor", + cursor: "", + expectedTime: time.Time{}, + expectedID: "", + expectError: false, + }, + { + name: "decode invalid base64", + cursor: "not-valid-base64!!!", + expectError: true, + errorContains: "invalid cursor format", + }, + { + name: "decode cursor without separator", + cursor: "bm9zZXBhcmF0b3I=", // "noseparator" in base64 + expectError: true, + errorContains: "invalid leaderboard config cursor format", + }, + { + name: "decode cursor with empty ID part", + cursor: "MjAyNC0wNi0xNVQxMjozMDo0NVp8", // "2024-06-15T12:30:45Z|" in base64 + expectError: true, + errorContains: "cursor decoded to empty ID", + }, + { + name: "decode valid cursor from round trip", + cursor: EncodeLeaderboardConfigCursor(testTime, "LC01ARZ3NDEKTSV4RRFFQ69G5FAV"), + expectedTime: testTime, + expectedID: "LC01ARZ3NDEKTSV4RRFFQ69G5FAV", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := DecodeLeaderboardConfigCursor(tt.cursor) + + if tt.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorContains) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedID, result.ID) + if !tt.expectedTime.IsZero() { + assert.True(t, tt.expectedTime.Equal(result.CreatedAt)) + } + } + }) + } +} + +func TestLeaderboardConfigCursorRoundTrip(t *testing.T) { + createdAt := time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC) + id := "LC01ARZ3NDEKTSV4RRFFQ69G5FAV" + + encoded := EncodeLeaderboardConfigCursor(createdAt, id) + decoded, err := DecodeLeaderboardConfigCursor(encoded) + + require.NoError(t, err) + assert.Equal(t, id, decoded.ID) + assert.True(t, createdAt.Equal(decoded.CreatedAt)) +} From 0d3968c9f54f108caf215f991be4273983e7323d Mon Sep 17 00:00:00 2001 From: JWSametz Date: Thu, 10 Sep 2026 14:07:52 +0200 Subject: [PATCH 7/9] feat(graphql): implement leaderboard config resolvers --- backend/internal/graph/api/leaderboards.go | 286 ++++++++++++++++++ .../graph/api/leaderboards.resolvers.go | 196 ++++++++++++ .../internal/graph/api/leaderboards_test.go | 200 ++++++++++++ 3 files changed, 682 insertions(+) create mode 100644 backend/internal/graph/api/leaderboards.go create mode 100644 backend/internal/graph/api/leaderboards.resolvers.go create mode 100644 backend/internal/graph/api/leaderboards_test.go diff --git a/backend/internal/graph/api/leaderboards.go b/backend/internal/graph/api/leaderboards.go new file mode 100644 index 00000000..ebedeb33 --- /dev/null +++ b/backend/internal/graph/api/leaderboards.go @@ -0,0 +1,286 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bcc-media/wayfarer/internal/database/sqlc" + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/bcc-media/wayfarer/internal/graph/pagination" + "github.com/bcc-media/wayfarer/internal/loaders" + "github.com/bcc-media/wayfarer/internal/middleware" + "github.com/bcc-media/wayfarer/internal/services" + "github.com/jackc/pgx/v5/pgtype" +) + +// defaultLeaderboardConfigsPageSize is the page size used by the admin leaderboardConfigs +// cursor query when neither first nor last is specified. Kept as a single constant so the +// two call sites (query-limit calculation, hasMore trimming) can't drift out of sync. +const defaultLeaderboardConfigsPageSize = 10 + +// marshalLeaderboardFilter serializes a LeaderboardFilter input into the JSON bytes +// stored in the leaderboard_configs.filter JSONB column. Returns nil for a nil filter. +func marshalLeaderboardFilter(filter *model.LeaderboardFilter) ([]byte, error) { + if filter == nil { + return nil, nil + } + b, err := json.Marshal(filter) + if err != nil { + return nil, fmt.Errorf("failed to serialize filter: %w", err) + } + return b, nil +} + +// getVisibleLeaderboardConfigsByProject loads all leaderboard configs for a project via the +// dataloader and filters out inactive ones unless the requesting user is an admin/superadmin. +func (r *Resolver) getVisibleLeaderboardConfigsByProject(ctx context.Context, projectID string) ([]model.LeaderboardConfig, error) { + thunk := r.Loaders.LeaderboardConfigsByProjectLoader.Load(ctx, projectID) + configs, err := thunk() + if err != nil { + return nil, fmt.Errorf("failed to load leaderboard configs: %w", err) + } + return r.filterVisibleLeaderboardConfigs(ctx, configs), nil +} + +// getVisibleLeaderboardConfigsByEvent loads all leaderboard configs for an event via the +// dataloader and filters out inactive ones unless the requesting user is an admin/superadmin. +func (r *Resolver) getVisibleLeaderboardConfigsByEvent(ctx context.Context, eventID string) ([]model.LeaderboardConfig, error) { + thunk := r.Loaders.LeaderboardConfigsByEventLoader.Load(ctx, eventID) + configs, err := thunk() + if err != nil { + return nil, fmt.Errorf("failed to load leaderboard configs: %w", err) + } + return r.filterVisibleLeaderboardConfigs(ctx, configs), nil +} + +// filterVisibleLeaderboardConfigs drops inactive configs for non-admin viewers. +func (r *Resolver) filterVisibleLeaderboardConfigs(ctx context.Context, configs []*model.LeaderboardConfig) []model.LeaderboardConfig { + isAdmin := false + if userID, ok := middleware.GetUserID(ctx); ok && userID != "" { + isAdmin = r.RoleService.IsAdmin(ctx, userID) + } + return filterConfigsByVisibility(configs, isAdmin) +} + +// filterConfigsByVisibility drops inactive configs unless the viewer is an admin. +func filterConfigsByVisibility(configs []*model.LeaderboardConfig, isAdmin bool) []model.LeaderboardConfig { + result := make([]model.LeaderboardConfig, 0, len(configs)) + for _, config := range configs { + if config.IsActive || isAdmin { + result = append(result, *config) + } + } + return result +} + +// buildLeaderboardParamsFromConfig adapts a persisted LeaderboardConfig plus pagination +// args into services.LeaderboardParams, and reports whether it's an event-scoped config +// (so the caller knows to call GetEventLeaderboard instead of GetProjectLeaderboard). +func buildLeaderboardParamsFromConfig(obj *model.LeaderboardConfig, first *int, after *string, last *int, before *string, userID string) (params services.LeaderboardParams, isEvent bool, err error) { + var filter *model.LeaderboardFilter + if obj.Filter != nil { + if err := json.Unmarshal([]byte(*obj.Filter), &filter); err != nil { + return services.LeaderboardParams{}, false, fmt.Errorf("failed to parse leaderboard config filter: %w", err) + } + } + + contextID := obj.ProjectID + isEvent = obj.EventID != nil + if isEvent { + contextID = *obj.EventID + } + + return services.LeaderboardParams{ + ContextID: contextID, + EntityType: obj.EntityType, + Filter: filter, + First: first, + After: after, + Last: last, + Before: before, + UserID: userID, + }, isEvent, nil +} + +// getLeaderboardForConfig computes the finished, paginated leaderboard for a persisted +// config by adapting it into services.LeaderboardParams and reusing the same leaderboard +// engine (caching, pagination, nearestChurchRivals) as the ad-hoc Project/Event.leaderboard fields. +func (r *Resolver) getLeaderboardForConfig(ctx context.Context, obj *model.LeaderboardConfig, first *int, after *string, last *int, before *string) (*model.LeaderboardConnection, error) { + currentUserID, ok := middleware.GetUserID(ctx) + if !ok || currentUserID == "" { + return nil, fmt.Errorf("user not authenticated") + } + + params, isEvent, err := buildLeaderboardParamsFromConfig(obj, first, after, last, before, currentUserID) + if err != nil { + return nil, err + } + + var entries []services.LeaderboardEntry + var meEntry *services.LeaderboardEntry + var totalCount int + var rivals []services.LeaderboardEntry + if isEvent { + entries, meEntry, totalCount, rivals, err = r.LeaderboardService.GetEventLeaderboard(ctx, params) + } else { + entries, meEntry, totalCount, rivals, err = r.LeaderboardService.GetProjectLeaderboard(ctx, params) + } + if err != nil { + return nil, fmt.Errorf("failed to get leaderboard: %w", err) + } + + if obj.EntityType == model.LeaderboardEntityTypePersons { + result := FilterPersonLeaderboardEntries(entries, totalCount, first, after) + entries = result.Entries + first = result.AdjustedFirst + } + + connection, err := buildLeaderboardConnection(ctx, entries, meEntry, totalCount, rivals, currentUserID, obj.EntityType, obj.ProjectID, r.Loaders, first, last, after, before) + if err != nil { + return nil, fmt.Errorf("failed to build leaderboard connection: %w", err) + } + + return connection, nil +} + +// getFilteredLeaderboardConfigs handles the admin-facing leaderboardConfigs cursor query. +func (r *Resolver) getFilteredLeaderboardConfigs(ctx context.Context, filter *model.LeaderboardConfigFilter, first *int, after *string, last *int, before *string) (*model.LeaderboardConfigConnection, error) { + var afterCursor, beforeCursor *pagination.LeaderboardConfigCursor + if after != nil && *after != "" { + decoded, err := pagination.DecodeLeaderboardConfigCursor(*after) + if err != nil { + return nil, fmt.Errorf("invalid after cursor: %w", err) + } + afterCursor = &decoded + } + if before != nil && *before != "" { + decoded, err := pagination.DecodeLeaderboardConfigCursor(*before) + if err != nil { + return nil, fmt.Errorf("invalid before cursor: %w", err) + } + beforeCursor = &decoded + } + + params, err := buildLeaderboardConfigFilterParamsCursor(filter, first, afterCursor, last, beforeCursor) + if err != nil { + return nil, err + } + + rows, err := r.DB.Queries.GetLeaderboardConfigsFilteredCursor(ctx, params) + if err != nil { + return nil, fmt.Errorf("failed to query leaderboard configs: %w", err) + } + + totalCount, err := r.DB.Queries.CountLeaderboardConfigsFiltered(ctx, buildCountLeaderboardConfigsFilterParams(filter)) + if err != nil { + return nil, fmt.Errorf("failed to count leaderboard configs: %w", err) + } + + requestedLimit := defaultLeaderboardConfigsPageSize + if first != nil { + requestedLimit = *first + } else if last != nil { + requestedLimit = *last + } + + hasMore := len(rows) > requestedLimit + configRows := rows + if hasMore { + configRows = rows[:requestedLimit] + } + + if last != nil { + for i, j := 0, len(configRows)-1; i < j; i, j = i+1, j-1 { + configRows[i], configRows[j] = configRows[j], configRows[i] + } + } + + configs := make([]*model.LeaderboardConfig, len(configRows)) + for i, row := range configRows { + configs[i] = loaders.ConvertRowToLeaderboardConfig(row) + } + + connection := pagination.BuildLeaderboardConfigConnection(pagination.BuildLeaderboardConfigConnectionParams{ + Configs: configs, + RequestedFirst: first, + RequestedLast: last, + RequestedAfter: after, + RequestedBefore: before, + TotalCount: int(totalCount), + HasMore: hasMore, + }) + + return connection, nil +} + +// buildLeaderboardConfigFilterParamsCursor converts a GraphQL filter to cursor query parameters +func buildLeaderboardConfigFilterParamsCursor(filter *model.LeaderboardConfigFilter, first *int, afterCursor *pagination.LeaderboardConfigCursor, last *int, beforeCursor *pagination.LeaderboardConfigCursor) (sqlc.GetLeaderboardConfigsFilteredCursorParams, error) { + params := sqlc.GetLeaderboardConfigsFilteredCursorParams{} + + if filter != nil { + if len(filter.Ids) > 0 { + params.Ids = filter.Ids + } + if filter.ProjectID != nil { + params.Projectid = *filter.ProjectID + } + if filter.EventID != nil { + params.Eventid = *filter.EventID + } + params.Isactive = filter.IsActive + } + + isBackward := false + var limit int + + if first != nil && last != nil { + return params, fmt.Errorf("cannot specify both first and last") + } + + if first != nil { + limit = *first + 1 + isBackward = false + } else if last != nil { + limit = *last + 1 + isBackward = true + } else { + limit = defaultLeaderboardConfigsPageSize + 1 + isBackward = false + } + + params.Querylimit = int32(limit) + params.Isbackward = isBackward + + if afterCursor != nil && afterCursor.ID != "" { + params.Aftercursorcreatedat = pgtype.Timestamptz{Time: afterCursor.CreatedAt, Valid: true} + params.Aftercursorid = afterCursor.ID + } + + if beforeCursor != nil && beforeCursor.ID != "" { + params.Beforecursorcreatedat = pgtype.Timestamptz{Time: beforeCursor.CreatedAt, Valid: true} + params.Beforecursorid = beforeCursor.ID + } + + return params, nil +} + +// buildCountLeaderboardConfigsFilterParams converts a GraphQL filter to count query parameters +func buildCountLeaderboardConfigsFilterParams(filter *model.LeaderboardConfigFilter) sqlc.CountLeaderboardConfigsFilteredParams { + params := sqlc.CountLeaderboardConfigsFilteredParams{} + + if filter != nil { + if len(filter.Ids) > 0 { + params.Ids = filter.Ids + } + if filter.ProjectID != nil { + params.Projectid = *filter.ProjectID + } + if filter.EventID != nil { + params.Eventid = *filter.EventID + } + params.Isactive = filter.IsActive + } + + return params +} diff --git a/backend/internal/graph/api/leaderboards.resolvers.go b/backend/internal/graph/api/leaderboards.resolvers.go new file mode 100644 index 00000000..49095471 --- /dev/null +++ b/backend/internal/graph/api/leaderboards.resolvers.go @@ -0,0 +1,196 @@ +package api + +// This file will be automatically regenerated based on the schema, any resolver implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.81 + +import ( + "context" + "errors" + "fmt" + + "github.com/bcc-media/wayfarer/internal/database/sqlc" + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/bcc-media/wayfarer/internal/loaders" + "github.com/bcc-media/wayfarer/internal/middleware" + "github.com/bcc-media/wayfarer/internal/ulid" + "github.com/jackc/pgx/v5/pgconn" +) + +// Leaderboards is the resolver for the leaderboards field. +func (r *eventResolver) Leaderboards(ctx context.Context, obj *model.Event) ([]model.LeaderboardConfig, error) { + return r.getVisibleLeaderboardConfigsByEvent(ctx, obj.ID) +} + +// Project is the resolver for the project field. +func (r *leaderboardConfigResolver) Project(ctx context.Context, obj *model.LeaderboardConfig) (*model.Project, error) { + return resolveProjectByID(ctx, r.Resolver, obj.ProjectID) +} + +// Event is the resolver for the event field. +func (r *leaderboardConfigResolver) Event(ctx context.Context, obj *model.LeaderboardConfig) (*model.Event, error) { + return resolveEventByID(ctx, r.Resolver, obj.EventID) +} + +// Leaderboard is the resolver for the leaderboard field. +func (r *leaderboardConfigResolver) Leaderboard(ctx context.Context, obj *model.LeaderboardConfig, first *int, after *string, last *int, before *string) (*model.LeaderboardConnection, error) { + return r.getLeaderboardForConfig(ctx, obj, first, after, last, before) +} + +// CreateLeaderboardConfig is the resolver for the createLeaderboardConfig field. +func (r *mutationResolver) CreateLeaderboardConfig(ctx context.Context, input model.CreateLeaderboardConfigInput) (*model.LeaderboardConfig, error) { + userID, ok := middleware.GetUserID(ctx) + if !ok || userID == "" { + return nil, fmt.Errorf("user not authenticated") + } + + if !r.RoleService.CanManageProject(ctx, userID, input.ProjectID) { + return nil, fmt.Errorf("unauthorized to create leaderboard configs in this project") + } + + filterBytes, err := marshalLeaderboardFilter(input.Filter) + if err != nil { + return nil, fmt.Errorf("invalid filter: %w", err) + } + + var sortOrder *int32 + if input.SortOrder != nil { + so := int32(*input.SortOrder) + sortOrder = &so + } + + params := sqlc.CreateLeaderboardConfigParams{ + ID: ulid.NewLeaderboardConfigID(), + Projectid: input.ProjectID, + Eventid: input.EventID, + Name: input.Name, + Slug: input.Slug, + Entitytype: string(input.EntityType), + Filter: filterBytes, + Sortorder: sortOrder, + Isactive: input.IsActive, + } + + row, err := r.DB.Queries.CreateLeaderboardConfig(ctx, params) + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + return nil, fmt.Errorf("a leaderboard config with this slug already exists in this project") + } + return nil, fmt.Errorf("failed to create leaderboard config: %w", err) + } + + r.Cache.InvalidateLeaderboardConfig(row.ID, row.ProjectID, row.EventID) + + return loaders.ConvertRowToLeaderboardConfig(row), nil +} + +// UpdateLeaderboardConfig is the resolver for the updateLeaderboardConfig field. +func (r *mutationResolver) UpdateLeaderboardConfig(ctx context.Context, id string, input model.UpdateLeaderboardConfigInput) (*model.LeaderboardConfig, error) { + userID, ok := middleware.GetUserID(ctx) + if !ok || userID == "" { + return nil, fmt.Errorf("user not authenticated") + } + + existing, err := r.DB.Queries.GetLeaderboardConfigByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("leaderboard config not found: %w", err) + } + + if !r.RoleService.CanManageProject(ctx, userID, existing.ProjectID) { + return nil, fmt.Errorf("unauthorized to update leaderboard configs in this project") + } + + var entityType *string + if input.EntityType != nil { + et := string(*input.EntityType) + entityType = &et + } + + filterBytes, err := marshalLeaderboardFilter(input.Filter) + if err != nil { + return nil, fmt.Errorf("invalid filter: %w", err) + } + + var sortOrder *int32 + if input.SortOrder != nil { + so := int32(*input.SortOrder) + sortOrder = &so + } + + params := sqlc.UpdateLeaderboardConfigParams{ + ID: id, + Name: input.Name, + Slug: input.Slug, + Entitytype: entityType, + Filter: filterBytes, + Clearfilter: input.ClearFilter, + Sortorder: sortOrder, + Isactive: input.IsActive, + } + + row, err := r.DB.Queries.UpdateLeaderboardConfig(ctx, params) + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + return nil, fmt.Errorf("a leaderboard config with this slug already exists in this project") + } + return nil, fmt.Errorf("failed to update leaderboard config: %w", err) + } + + r.Cache.InvalidateLeaderboardConfig(row.ID, row.ProjectID, row.EventID) + + return loaders.ConvertRowToLeaderboardConfig(row), nil +} + +// DeleteLeaderboardConfig is the resolver for the deleteLeaderboardConfig field. +func (r *mutationResolver) DeleteLeaderboardConfig(ctx context.Context, id string) (bool, error) { + userID, ok := middleware.GetUserID(ctx) + if !ok || userID == "" { + return false, fmt.Errorf("user not authenticated") + } + + existing, err := r.DB.Queries.GetLeaderboardConfigByID(ctx, id) + if err != nil { + return false, fmt.Errorf("leaderboard config not found: %w", err) + } + + if !r.RoleService.CanManageProject(ctx, userID, existing.ProjectID) { + return false, fmt.Errorf("unauthorized to delete leaderboard configs in this project") + } + + if err := r.DB.Queries.DeleteLeaderboardConfig(ctx, id); err != nil { + return false, fmt.Errorf("failed to delete leaderboard config: %w", err) + } + + r.Cache.InvalidateLeaderboardConfig(existing.ID, existing.ProjectID, existing.EventID) + + return true, nil +} + +// Leaderboards is the resolver for the leaderboards field. +func (r *projectResolver) Leaderboards(ctx context.Context, obj *model.Project) ([]model.LeaderboardConfig, error) { + return r.getVisibleLeaderboardConfigsByProject(ctx, obj.ID) +} + +// LeaderboardConfig is the resolver for the leaderboardConfig field. +func (r *queryResolver) LeaderboardConfig(ctx context.Context, id string) (*model.LeaderboardConfig, error) { + thunk := r.Loaders.LeaderboardConfigByIDLoader.Load(ctx, id) + config, err := thunk() + if err != nil { + return nil, fmt.Errorf("failed to load leaderboard config: %w", err) + } + return config, nil +} + +// LeaderboardConfigs is the resolver for the leaderboardConfigs field. +func (r *queryResolver) LeaderboardConfigs(ctx context.Context, filter *model.LeaderboardConfigFilter, first *int, after *string, last *int, before *string) (*model.LeaderboardConfigConnection, error) { + return r.getFilteredLeaderboardConfigs(ctx, filter, first, after, last, before) +} + +// LeaderboardConfig returns LeaderboardConfigResolver implementation. +func (r *Resolver) LeaderboardConfig() LeaderboardConfigResolver { + return &leaderboardConfigResolver{r} +} + +type leaderboardConfigResolver struct{ *Resolver } diff --git a/backend/internal/graph/api/leaderboards_test.go b/backend/internal/graph/api/leaderboards_test.go new file mode 100644 index 00000000..ba6adf1c --- /dev/null +++ b/backend/internal/graph/api/leaderboards_test.go @@ -0,0 +1,200 @@ +package api + +import ( + "testing" + + "github.com/bcc-media/wayfarer/internal/graph/api/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMarshalLeaderboardFilter_Nil(t *testing.T) { + b, err := marshalLeaderboardFilter(nil) + require.NoError(t, err) + assert.Nil(t, b) +} + +func TestMarshalLeaderboardFilter_ValidFilter(t *testing.T) { + minScore := 10 + churchID := "CH01ARZ3NDEKTSV4RRFFQ69G5FAV" + filter := &model.LeaderboardFilter{ + MinScore: &minScore, + ChurchID: &churchID, + } + + b, err := marshalLeaderboardFilter(filter) + require.NoError(t, err) + require.NotNil(t, b) + assert.Contains(t, string(b), `"minScore":10`) + assert.Contains(t, string(b), churchID) +} + +func TestFilterConfigsByVisibility_NonAdminSeesOnlyActive(t *testing.T) { + configs := []*model.LeaderboardConfig{ + {ID: "LC1", IsActive: true}, + {ID: "LC2", IsActive: false}, + {ID: "LC3", IsActive: true}, + } + + result := filterConfigsByVisibility(configs, false) + + require.Len(t, result, 2) + assert.Equal(t, "LC1", result[0].ID) + assert.Equal(t, "LC3", result[1].ID) +} + +func TestFilterConfigsByVisibility_AdminSeesAll(t *testing.T) { + configs := []*model.LeaderboardConfig{ + {ID: "LC1", IsActive: true}, + {ID: "LC2", IsActive: false}, + } + + result := filterConfigsByVisibility(configs, true) + + require.Len(t, result, 2) +} + +func TestFilterConfigsByVisibility_EmptyInput(t *testing.T) { + result := filterConfigsByVisibility(nil, false) + assert.Empty(t, result) +} + +func TestBuildLeaderboardParamsFromConfig_ProjectScoped(t *testing.T) { + projectID := "PR01ARZ3NDEKTSV4RRFFQ69G5FAV" + config := &model.LeaderboardConfig{ + ProjectID: projectID, + EventID: nil, + EntityType: model.LeaderboardEntityTypePersons, + } + + params, isEvent, err := buildLeaderboardParamsFromConfig(config, nil, nil, nil, nil, "US01ARZ3NDEKTSV4RRFFQ69G5FAV") + + require.NoError(t, err) + assert.False(t, isEvent) + assert.Equal(t, projectID, params.ContextID) + assert.Equal(t, model.LeaderboardEntityTypePersons, params.EntityType) + assert.Equal(t, "US01ARZ3NDEKTSV4RRFFQ69G5FAV", params.UserID) + assert.Nil(t, params.Filter) +} + +func TestBuildLeaderboardParamsFromConfig_EventScoped(t *testing.T) { + projectID := "PR01ARZ3NDEKTSV4RRFFQ69G5FAV" + eventID := "EV01ARZ3NDEKTSV4RRFFQ69G5FAV" + config := &model.LeaderboardConfig{ + ProjectID: projectID, + EventID: &eventID, + EntityType: model.LeaderboardEntityTypeTeams, + } + + params, isEvent, err := buildLeaderboardParamsFromConfig(config, nil, nil, nil, nil, "US01ARZ3NDEKTSV4RRFFQ69G5FAV") + + require.NoError(t, err) + assert.True(t, isEvent) + assert.Equal(t, eventID, params.ContextID) +} + +func TestBuildLeaderboardParamsFromConfig_ParsesFilter(t *testing.T) { + filterJSON := `{"minScore":42}` + config := &model.LeaderboardConfig{ + ProjectID: "PR01ARZ3NDEKTSV4RRFFQ69G5FAV", + EntityType: model.LeaderboardEntityTypePersons, + Filter: &filterJSON, + } + + params, _, err := buildLeaderboardParamsFromConfig(config, nil, nil, nil, nil, "US01ARZ3NDEKTSV4RRFFQ69G5FAV") + + require.NoError(t, err) + require.NotNil(t, params.Filter) + require.NotNil(t, params.Filter.MinScore) + assert.Equal(t, 42, *params.Filter.MinScore) +} + +func TestBuildLeaderboardParamsFromConfig_InvalidFilterJSON(t *testing.T) { + invalidJSON := `{not valid json` + config := &model.LeaderboardConfig{ + ProjectID: "PR01ARZ3NDEKTSV4RRFFQ69G5FAV", + EntityType: model.LeaderboardEntityTypePersons, + Filter: &invalidJSON, + } + + _, _, err := buildLeaderboardParamsFromConfig(config, nil, nil, nil, nil, "US01ARZ3NDEKTSV4RRFFQ69G5FAV") + + assert.Error(t, err) +} + +func TestBuildLeaderboardConfigFilterParamsCursor_AppliesFilterFields(t *testing.T) { + projectID := "PR01ARZ3NDEKTSV4RRFFQ69G5FAV" + isActive := true + filter := &model.LeaderboardConfigFilter{ + ProjectID: &projectID, + IsActive: &isActive, + } + + params, err := buildLeaderboardConfigFilterParamsCursor(filter, nil, nil, nil, nil) + + require.NoError(t, err) + assert.Equal(t, projectID, params.Projectid) + require.NotNil(t, params.Isactive) + assert.True(t, *params.Isactive) + assert.Equal(t, int32(11), params.Querylimit) // default page size + 1 + assert.False(t, params.Isbackward) +} + +func TestBuildLeaderboardConfigFilterParamsCursor_FirstAndLastMutuallyExclusive(t *testing.T) { + first := 5 + last := 5 + + _, err := buildLeaderboardConfigFilterParamsCursor(nil, &first, nil, &last, nil) + + assert.Error(t, err) +} + +func TestBuildLeaderboardConfigFilterParamsCursor_BackwardPagination(t *testing.T) { + last := 5 + + params, err := buildLeaderboardConfigFilterParamsCursor(nil, nil, nil, &last, nil) + + require.NoError(t, err) + assert.True(t, params.Isbackward) + assert.Equal(t, int32(6), params.Querylimit) +} + +func TestBuildLeaderboardConfigFilterParamsCursor_EmptyIdsTreatedAsNoFilter(t *testing.T) { + filter := &model.LeaderboardConfigFilter{ + Ids: []string{}, + } + + params, err := buildLeaderboardConfigFilterParamsCursor(filter, nil, nil, nil, nil) + + require.NoError(t, err) + assert.Nil(t, params.Ids, "an explicitly empty ids slice should not filter out every row") +} + +func TestBuildCountLeaderboardConfigsFilterParams_NilFilter(t *testing.T) { + params := buildCountLeaderboardConfigsFilterParams(nil) + assert.Equal(t, "", params.Projectid) + assert.Nil(t, params.Isactive) +} + +func TestBuildCountLeaderboardConfigsFilterParams_WithFilter(t *testing.T) { + eventID := "EV01ARZ3NDEKTSV4RRFFQ69G5FAV" + filter := &model.LeaderboardConfigFilter{ + EventID: &eventID, + Ids: []string{"LC1", "LC2"}, + } + + params := buildCountLeaderboardConfigsFilterParams(filter) + + assert.Equal(t, eventID, params.Eventid) + assert.Equal(t, []string{"LC1", "LC2"}, params.Ids) +} + +func TestBuildCountLeaderboardConfigsFilterParams_EmptyIdsTreatedAsNoFilter(t *testing.T) { + filter := &model.LeaderboardConfigFilter{ + Ids: []string{}, + } + + params := buildCountLeaderboardConfigsFilterParams(filter) + + assert.Nil(t, params.Ids, "an explicitly empty ids slice should not filter out every row") +} From 12dd0e81c07d50652da59afa43b09a3907a8daa8 Mon Sep 17 00:00:00 2001 From: JWSametz Date: Fri, 11 Sep 2026 09:11:38 +0200 Subject: [PATCH 8/9] chore(api): regenerate frontend GraphQL types --- frontend/app/api/generated.ts | 122 ++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/frontend/app/api/generated.ts b/frontend/app/api/generated.ts index c9ead6c2..c0d606e0 100644 --- a/frontend/app/api/generated.ts +++ b/frontend/app/api/generated.ts @@ -488,6 +488,17 @@ export type CreateEventInput = { startDate: Scalars['DateTime']['input']; }; +export type CreateLeaderboardConfigInput = { + entityType: LeaderboardEntityType; + eventId?: InputMaybe; + filter?: InputMaybe; + isActive?: InputMaybe; + name: Scalars['String']['input']; + projectId: Scalars['ID']['input']; + slug: Scalars['String']['input']; + sortOrder?: InputMaybe; +}; + export type CreateOrderingItemInput = { correctOrder: Scalars['Int']['input']; itemText: Scalars['String']['input']; @@ -675,7 +686,10 @@ export type Event = { description: Scalars['String']['output']; endDate: Scalars['DateTime']['output']; id: Scalars['ID']['output']; + /** @deprecated Use `leaderboards` (LeaderboardConfig) for persisted, admin-managed leaderboards instead. */ leaderboard: LeaderboardConnection; + /** Active leaderboard configs for this event (all configs, including inactive, for admins/superadmins). */ + leaderboards: Array; name: Scalars['String']['output']; parentProject: Project; startDate: Scalars['DateTime']['output']; @@ -942,14 +956,70 @@ export type JsonResponse = QuizResponse & { timeSpentSeconds?: Maybe; }; +export type LeaderboardConfig = { + __typename?: 'LeaderboardConfig'; + createdAt: Scalars['DateTime']['output']; + entityType: LeaderboardEntityType; + event?: Maybe; + /** The filter applied to this leaderboard, mirroring the `LeaderboardFilter` input shape. */ + filter?: Maybe; + id: Scalars['ID']['output']; + isActive: Scalars['Boolean']['output']; + /** The finished, computed leaderboard for this config. */ + leaderboard: LeaderboardConnection; + name: Scalars['String']['output']; + project: Project; + slug: Scalars['String']['output']; + sortOrder: Scalars['Int']['output']; + updatedAt: Scalars['DateTime']['output']; +}; + + +export type LeaderboardConfigLeaderboardArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + +export type LeaderboardConfigConnection = { + __typename?: 'LeaderboardConfigConnection'; + edges: Array; + pageInfo: PageInfo; + totalCount: Scalars['Int']['output']; +}; + +export type LeaderboardConfigEdge = { + __typename?: 'LeaderboardConfigEdge'; + cursor: Scalars['String']['output']; + node: LeaderboardConfig; +}; + +export type LeaderboardConfigFilter = { + eventId?: InputMaybe; + ids?: InputMaybe>; + isActive?: InputMaybe; + projectId?: InputMaybe; +}; + export type LeaderboardConnection = { __typename?: 'LeaderboardConnection'; edges: Array; me?: Maybe; + /** + * Nearest same-church entries ranked above the viewer on a PERSONS leaderboard. + * Empty for other entity types or when the viewer isn't on the board. + */ + nearestChurchRivals: Array; pageInfo: PageInfo; totalCount: Scalars['Int']['output']; }; + +export type LeaderboardConnectionNearestChurchRivalsArgs = { + first?: InputMaybe; +}; + export type LeaderboardEdge = { __typename?: 'LeaderboardEdge'; cursor: Scalars['String']['output']; @@ -1075,6 +1145,7 @@ export type Mutation = { createContentAchievement: ContentAchievement; createContentAchievementFromExternalContent: ContentAchievement; createEvent: Event; + createLeaderboardConfig: LeaderboardConfig; createProject: Project; createQuiz: Quiz; createQuizAchievement: QuizAchievement; @@ -1091,6 +1162,7 @@ export type Mutation = { deleteChallenge: Scalars['Boolean']['output']; deleteEvent: Scalars['Boolean']['output']; deleteFeedback: Scalars['Boolean']['output']; + deleteLeaderboardConfig: Scalars['Boolean']['output']; deleteProject: Scalars['Boolean']['output']; deleteQuiz: Scalars['Boolean']['output']; deleteQuizQuestion: Scalars['Boolean']['output']; @@ -1166,6 +1238,7 @@ export type Mutation = { updateContentAchievement: ContentAchievement; updateEvent: Event; updateFeedbackTags: UserFeedback; + updateLeaderboardConfig: LeaderboardConfig; updateProject: Project; updateQuiz: Quiz; updateQuizAchievement: QuizAchievement; @@ -1375,6 +1448,11 @@ export type MutationCreateEventArgs = { }; +export type MutationCreateLeaderboardConfigArgs = { + input: CreateLeaderboardConfigInput; +}; + + export type MutationCreateProjectArgs = { input: CreateProjectInput; }; @@ -1460,6 +1538,11 @@ export type MutationDeleteFeedbackArgs = { }; +export type MutationDeleteLeaderboardConfigArgs = { + id: Scalars['ID']['input']; +}; + + export type MutationDeleteProjectArgs = { id: Scalars['ID']['input']; }; @@ -1866,6 +1949,12 @@ export type MutationUpdateFeedbackTagsArgs = { }; +export type MutationUpdateLeaderboardConfigArgs = { + id: Scalars['ID']['input']; + input: UpdateLeaderboardConfigInput; +}; + + export type MutationUpdateProjectArgs = { id: Scalars['ID']['input']; input: UpdateProjectInput; @@ -2076,7 +2165,10 @@ export type Project = { infoMessageEnd?: Maybe; infoMessageStart?: Maybe; journal: ScoreJournalConnection; + /** @deprecated Use `leaderboards` (LeaderboardConfig) for persisted, admin-managed leaderboards instead. */ leaderboard: LeaderboardConnection; + /** Active leaderboard configs for this project (all configs, including inactive, for admins/superadmins). */ + leaderboards: Array; myChurchTeams: Array; myPoints: Scalars['Int']['output']; myTeam?: Maybe; @@ -2171,6 +2263,8 @@ export type Query = { firebaseToken: FirebaseTokenResponse; frontendConfig: Scalars['JSON']['output']; instanceID: Scalars['String']['output']; + leaderboardConfig: LeaderboardConfig; + leaderboardConfigs: LeaderboardConfigConnection; me: User; myBulkJobs: Array; myCurrentEvent: Event; @@ -2332,6 +2426,20 @@ export type QueryFileUploadArgs = { }; +export type QueryLeaderboardConfigArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryLeaderboardConfigsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + export type QueryMyBulkJobsArgs = { limit?: InputMaybe; }; @@ -3096,6 +3204,20 @@ export type UpdateEventInput = { startDate?: InputMaybe; }; +export type UpdateLeaderboardConfigInput = { + /** + * Set to true to remove the existing filter entirely (show an unfiltered leaderboard). + * Ignored if `filter` is also provided. Has no effect otherwise. + */ + clearFilter?: InputMaybe; + entityType?: InputMaybe; + filter?: InputMaybe; + isActive?: InputMaybe; + name?: InputMaybe; + slug?: InputMaybe; + sortOrder?: InputMaybe; +}; + export type UpdateProjectInput = { branding?: InputMaybe; description?: InputMaybe; From ce00706fccb84e57925ae2d56d59ae8d78af9504 Mon Sep 17 00:00:00 2001 From: JWSametz Date: Fri, 11 Sep 2026 09:14:42 +0200 Subject: [PATCH 9/9] docs(leaderboards): document persisted leaderboard configs --- notes/leaderboard-configs.md | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 notes/leaderboard-configs.md diff --git a/notes/leaderboard-configs.md b/notes/leaderboard-configs.md new file mode 100644 index 00000000..e954d4f4 --- /dev/null +++ b/notes/leaderboard-configs.md @@ -0,0 +1,44 @@ +# Persisted Leaderboard Configs + +## Why + +Previously all leaderboards were entirely ad-hoc: clients called `Project.leaderboard(entityType, filter, ...)` / `Event.leaderboard(...)` and constructed the query at request time. There was no way for an admin to define a named, reusable leaderboard once and have it served as a finished result. `LeaderboardConfig` adds that: a persisted, project-scoped (optionally event-scoped) entity admins manage via CRUD, served to clients as a ready-made `LeaderboardConnection`. + +The old ad-hoc `leaderboard(...)` fields on `Project`/`Event` are **deprecated but not removed** — existing frontend call sites keep working while they migrate. + +## Database + +- **Migration**: `00102_add_leaderboard_configs.sql` +- **Table**: `leaderboard_configs` + - `project_id CHAR(28) NOT NULL REFERENCES projects(id) ON DELETE CASCADE` + - `event_id CHAR(28) REFERENCES events(id) ON DELETE SET NULL` — nullable, same optional-scoping pattern as `challenges.event_id` + - `name`, `slug` (unique per project via `idx_leaderboard_configs_project_slug`) + - `entity_type VARCHAR(20)` — mirrors `LeaderboardEntityType` (`PERSONS`/`TEAMS`/`SUPERTEAMS`/`CHURCHES`), enforced by a `CHECK` constraint + - `filter JSONB` — nullable. Stores the same shape as the GraphQL `LeaderboardFilter` input, serialized via `json.Marshal`/`json.Unmarshal` (same "typed struct <-> JSONB `[]byte`" convention as `push_notification_log.target_criteria`, see `internal/services/push/service.go`). No typed columns per filter field — this avoids a migration every time `LeaderboardFilter` grows a field. + - `sort_order INT`, `is_active BOOLEAN` (draft/inactive configs are hidden from non-admin callers) +- **ID prefix**: `LC` (`ulid.NewLeaderboardConfigID()` / `ulid.IsLeaderboardConfigID()`) + +## GraphQL + +- **Schema**: `gql/leaderboards.graphqls` +- **Type**: `LeaderboardConfig` — `project`/`event`/`leaderboard` are resolver fields (`@goField(forceResolver: true)`). `filter` is exposed as the `JSON` scalar (raw string on the wire, matching this codebase's existing `JSON` scalar convention — it binds to plain Go `string`, NOT a map, see `webhooks_helpers.go`/`settings.resolvers.go`/`quiz_helpers.go` for other `JSON`-scalar usages). The **input** side (`CreateLeaderboardConfigInput.filter` / `UpdateLeaderboardConfigInput.filter`) uses the typed `LeaderboardFilter` input instead — `LeaderboardFilter` can't be reused as an output field type since GraphQL forbids using an `input` type as an object field's type. +- **Admin CRUD**: `createLeaderboardConfig` / `updateLeaderboardConfig` / `deleteLeaderboardConfig` mutations, `leaderboardConfig(id)` / `leaderboardConfigs(filter, ...)` queries — all behind `@requireRole(roles: ["admin", "superadmin"])`. The two queries are a deliberate, confirmed exception to the "`@requireRole` is mutation-only" convention, since they exist purely to expose admin CRUD-management data (including inactive/draft configs). + - `UpdateLeaderboardConfigInput.clearFilter: Boolean` — since a plain `COALESCE(sqlc.narg('filter'), filter)` update can't distinguish "not provided" from "explicitly clear," `clearFilter: true` forces `filter` to `NULL` (ignored if `filter` is also provided in the same call). This is the one field on this entity where clearing back to unfiltered is a realistic admin action; other `Update*Input`s in this codebase (e.g. `UpdateChallengeInput.imageUrl`/`.url`) have the same unaddressed limitation but weren't changed as part of this work. +- **Serving**: `Project.leaderboards` / `Event.leaderboards` return all *active* configs (all configs, including inactive, if the caller is admin/superadmin — checked via `RoleService.IsAdmin` in the resolver, not the schema). Each config's `leaderboard(first, after, last, before)` field returns the fully computed `LeaderboardConnection`. + +## Reused leaderboard engine + +`LeaderboardConfig.leaderboard` does **not** reimplement leaderboard computation — it adapts a config into the existing `services.LeaderboardParams` (see `backend/internal/graph/api/leaderboards.go:buildLeaderboardParamsFromConfig`) and calls the same `LeaderboardService.GetProjectLeaderboard`/`GetEventLeaderboard` used by the ad-hoc fields, then the same `buildLeaderboardConnection`/`FilterPersonLeaderboardEntries` helpers. This means: +- Caching, pagination (rank-based cursors), and `nearestChurchRivals` all work identically whether a leaderboard was reached via a config or the old ad-hoc query. +- The full-board cache key is derived from `(context, contextID, entityType, filterMap)` — not from how the request arrived — so a config and an ad-hoc query with matching project/entityType/filters correctly share one cache entry. + +## Files touched + +- Migration: `backend/internal/database/migrations/00102_add_leaderboard_configs.sql` +- ULID: `backend/internal/ulid/ulid.go` +- Schema: `gql/leaderboards.graphqls`, `gql/projects.graphqls`, `gql/events.graphqls` (deprecations) +- sqlc: `backend/internal/database/queries/leaderboard_configs.sql` +- Cache: `backend/internal/cache/{keys,invalidation,sync}.go` — `InvalidateLeaderboardConfig`, following the same per-entity pattern as `InvalidateChallenge` +- Dataloaders: `backend/internal/loaders/leaderboard_config_by_id.go`, `leaderboard_configs_by_project.go`, `leaderboard_configs_by_event.go` +- Resolvers/helpers: `backend/internal/graph/api/leaderboards.resolvers.go`, `leaderboards.go` +- Pagination: `backend/internal/graph/pagination/cursor.go` (`LeaderboardConfigCursor`), `connection.go` (`BuildLeaderboardConfigConnection`) — same per-entity cursor/connection pattern as `ChallengeCursor`/`BuildChallengeConnection`