mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 20:31:48 +00:00
212 lines
6.8 KiB
Go
212 lines
6.8 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"strings"
|
|
|
|
"github.com/keegancsmith/sqlf"
|
|
"github.com/sourcegraph/jsonx"
|
|
|
|
"github.com/sourcegraph/sourcegraph/internal/api"
|
|
"github.com/sourcegraph/sourcegraph/internal/conf"
|
|
"github.com/sourcegraph/sourcegraph/internal/database/basestore"
|
|
"github.com/sourcegraph/sourcegraph/internal/jsonc"
|
|
"github.com/sourcegraph/sourcegraph/internal/trace"
|
|
"github.com/sourcegraph/sourcegraph/lib/errors"
|
|
"github.com/sourcegraph/sourcegraph/schema"
|
|
)
|
|
|
|
type SettingsStore interface {
|
|
CreateIfUpToDate(ctx context.Context, subject api.SettingsSubject, lastID *int32, authorUserID *int32, contents string) (*api.Settings, error)
|
|
Done(error) error
|
|
GetLastestSchemaSettings(context.Context, api.SettingsSubject) (*schema.Settings, error)
|
|
GetLatest(context.Context, api.SettingsSubject) (*api.Settings, error)
|
|
ListAll(ctx context.Context, impreciseSubstring string) ([]*api.Settings, error)
|
|
Transact(context.Context) (SettingsStore, error)
|
|
With(basestore.ShareableStore) SettingsStore
|
|
basestore.ShareableStore
|
|
}
|
|
|
|
type settingsStore struct {
|
|
*basestore.Store
|
|
}
|
|
|
|
// SettingsWith instantiates and returns a new SettingsStore using the other store handle.
|
|
func SettingsWith(other basestore.ShareableStore) SettingsStore {
|
|
return &settingsStore{Store: basestore.NewWithHandle(other.Handle())}
|
|
}
|
|
|
|
func (s *settingsStore) With(other basestore.ShareableStore) SettingsStore {
|
|
return &settingsStore{Store: s.Store.With(other)}
|
|
}
|
|
|
|
func (s *settingsStore) Transact(ctx context.Context) (SettingsStore, error) {
|
|
txBase, err := s.Store.Transact(ctx)
|
|
return &settingsStore{Store: txBase}, err
|
|
}
|
|
|
|
func (o *settingsStore) CreateIfUpToDate(ctx context.Context, subject api.SettingsSubject, lastID *int32, authorUserID *int32, contents string) (latestSetting *api.Settings, err error) {
|
|
if strings.TrimSpace(contents) == "" {
|
|
return nil, errors.Errorf("blank settings are invalid (you can clear the settings by entering an empty JSON object: {})")
|
|
}
|
|
|
|
// Validate JSON syntax before saving.
|
|
if _, errs := jsonx.Parse(contents, jsonx.ParseOptions{Comments: true, TrailingCommas: true}); len(errs) > 0 {
|
|
return nil, errors.Errorf("invalid settings JSON: %v", errs)
|
|
}
|
|
|
|
// Validate setting schema
|
|
problems, err := conf.ValidateSetting(contents)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(problems) > 0 {
|
|
return nil, errors.Errorf("invalid settings: %s", strings.Join(problems, ","))
|
|
}
|
|
|
|
s := api.Settings{
|
|
Subject: subject,
|
|
AuthorUserID: authorUserID,
|
|
Contents: contents,
|
|
}
|
|
|
|
tx, err := o.Transact(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
err = tx.Done(err)
|
|
}()
|
|
|
|
latestSetting, err = tx.GetLatest(ctx, subject)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
creatorIsUpToDate := latestSetting != nil && lastID != nil && latestSetting.ID == *lastID
|
|
if latestSetting == nil || creatorIsUpToDate {
|
|
err := tx.Handle().DB().QueryRowContext(
|
|
ctx,
|
|
"INSERT INTO settings(org_id, user_id, author_user_id, contents) VALUES($1, $2, $3, $4) RETURNING id, created_at",
|
|
s.Subject.Org, s.Subject.User, s.AuthorUserID, s.Contents).Scan(&s.ID, &s.CreatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
latestSetting = &s
|
|
}
|
|
|
|
return latestSetting, nil
|
|
}
|
|
|
|
func (o *settingsStore) GetLatest(ctx context.Context, subject api.SettingsSubject) (*api.Settings, error) {
|
|
var cond *sqlf.Query
|
|
switch {
|
|
case subject.Org != nil:
|
|
cond = sqlf.Sprintf("org_id=%d", *subject.Org)
|
|
case subject.User != nil:
|
|
cond = sqlf.Sprintf("user_id=%d AND EXISTS (SELECT NULL FROM users WHERE id=%d AND deleted_at IS NULL)", *subject.User, *subject.User)
|
|
default:
|
|
// No org and no user represents global site settings.
|
|
cond = sqlf.Sprintf("user_id IS NULL AND org_id IS NULL")
|
|
}
|
|
|
|
q := sqlf.Sprintf(`
|
|
SELECT s.id, s.org_id, s.user_id, CASE WHEN users.deleted_at IS NULL THEN s.author_user_id ELSE NULL END, s.contents, s.created_at FROM settings s
|
|
LEFT JOIN users ON users.id=s.author_user_id
|
|
WHERE %s
|
|
ORDER BY id DESC LIMIT 1`, cond)
|
|
rows, err := o.Query(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
settings, err := parseQueryRows(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(settings) != 1 {
|
|
// No configuration has been set for this subject yet.
|
|
return nil, nil
|
|
}
|
|
return settings[0], nil
|
|
}
|
|
|
|
func (o *settingsStore) GetLastestSchemaSettings(ctx context.Context, subject api.SettingsSubject) (*schema.Settings, error) {
|
|
apiSettings, err := o.GetLatest(ctx, subject)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if apiSettings == nil {
|
|
// Settings have never been saved for this subject; equivalent to `{}`.
|
|
return &schema.Settings{}, nil
|
|
}
|
|
|
|
var v schema.Settings
|
|
if err := jsonc.Unmarshal(apiSettings.Contents, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &v, nil
|
|
|
|
}
|
|
|
|
// ListAll lists ALL settings (across all users, orgs, etc).
|
|
//
|
|
// If impreciseSubstring is given, only settings whose raw JSONC string contains the substring are
|
|
// returned. This is only intended for use by migration code that needs to update settings, where
|
|
// limiting to matching settings can significantly narrow the amount of work necessary. For example,
|
|
// a migration to rename a settings property `foo` to `bar` would narrow the search space by only
|
|
// processing settings that contain the string `foo` (which will yield false positives, but that's
|
|
// acceptable because the migration shouldn't modify those results anyway).
|
|
//
|
|
// 🚨 SECURITY: This method does NOT verify the user is an admin. The caller is
|
|
// responsible for ensuring this or that the response never makes it to a user.
|
|
func (o *settingsStore) ListAll(ctx context.Context, impreciseSubstring string) (_ []*api.Settings, err error) {
|
|
tr, ctx := trace.New(ctx, "database.Settings.ListAll", "")
|
|
defer func() {
|
|
tr.SetError(err)
|
|
tr.Finish()
|
|
}()
|
|
|
|
q := sqlf.Sprintf(`
|
|
WITH q AS (
|
|
SELECT DISTINCT
|
|
ON (org_id, user_id, author_user_id)
|
|
id, org_id, user_id, author_user_id, contents, created_at
|
|
FROM settings
|
|
ORDER BY org_id, user_id, author_user_id, id DESC
|
|
)
|
|
SELECT q.id, q.org_id, q.user_id, CASE WHEN users.deleted_at IS NULL THEN q.author_user_id ELSE NULL END, q.contents, q.created_at
|
|
FROM q
|
|
LEFT JOIN users ON users.id=q.author_user_id
|
|
WHERE contents LIKE %s
|
|
ORDER BY q.org_id, q.user_id, q.author_user_id, q.id DESC
|
|
`, "%"+impreciseSubstring+"%")
|
|
rows, err := o.Query(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseQueryRows(rows)
|
|
}
|
|
|
|
func parseQueryRows(rows *sql.Rows) ([]*api.Settings, error) {
|
|
settings := []*api.Settings{}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
s := api.Settings{}
|
|
err := rows.Scan(&s.ID, &s.Subject.Org, &s.Subject.User, &s.AuthorUserID, &s.Contents, &s.CreatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.Subject.Org == nil && s.Subject.User == nil {
|
|
s.Subject.Site = true
|
|
}
|
|
settings = append(settings, &s)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return settings, nil
|
|
}
|