CARLOS rastrillo docs

🤖 objrec

Use objrec to keep small JSON records in your app's object store instead of a database. It suits an app whose records are few and small and looked up by key, like a sign-in home or a directory. Such an app then has nothing to back up or migrate.

Every write is conditional. Create succeeds once per key. Update succeeds only if nobody has written since you read. When a write loses, read again and decide.

store, err := objrec.S3FromEnv()
if err != nil {
	return err
}
if store == nil {
	store = objrec.Dir("records") // no bucket: local development
}

// An address can belong to one user only: the first create wins.
if _, err := objrec.CreateJSON(ctx, store, "emails/"+hash+".json", emailRecord{UserID: uid}); errors.Is(err, objrec.ErrExists) {
	// Someone already signed up with this address.
}

// Change a record without losing a concurrent change.
user, _, err := objrec.Mutate(ctx, store, "users/"+uid+".json", func(u User, exists bool) (User, error) {
	u.Epoch++
	return u, nil
})

Keys are lowercase letters, digits, ., _ and -, in segments separated by /. A record is at most MaxRecord bytes. Bytes of any size belong in blobs.

API

type Version string

var (
	ErrExists   = errors.New("objrec: record exists")
	ErrChanged  = errors.New("objrec: record changed")
	ErrNotFound = errors.New("objrec: record not found")
	ErrKey      = errors.New("objrec: invalid key")
	ErrTooLarge = errors.New("objrec: record too large")
)

const MaxRecord = 256 << 10

type Store interface {
	Get(ctx context.Context, key string) ([]byte, Version, error)
	Create(ctx context.Context, key string, body []byte) (Version, error)
	Update(ctx context.Context, key string, body []byte, v Version) (Version, error)
	Delete(ctx context.Context, key string) error
	List(ctx context.Context, prefix, cursor string, limit int) (keys []string, next string, err error)
}

func ValidKey(key string) error
func ValidPrefix(prefix string) error

func GetJSON[T any](ctx context.Context, s Store, key string) (T, Version, error)
func CreateJSON[T any](ctx context.Context, s Store, key string, v T) (Version, error)
func UpdateJSON[T any](ctx context.Context, s Store, key string, v T, ver Version) (Version, error)
func Mutate[T any](ctx context.Context, s Store, key string, fn func(cur T, exists bool) (T, error)) (T, Version, error)

type DirStore struct { /* unexported fields */ }
func Dir(root string) *DirStore
func (d *DirStore) Get(ctx context.Context, key string) ([]byte, Version, error)
func (d *DirStore) Create(ctx context.Context, key string, body []byte) (Version, error)
func (d *DirStore) Update(ctx context.Context, key string, body []byte, v Version) (Version, error)
func (d *DirStore) Delete(ctx context.Context, key string) error
func (d *DirStore) List(ctx context.Context, prefix, cursor string, limit int) ([]string, string, error)

type S3Config struct {
	Bucket, Region, Endpoint, AccessKey, SecretKey, Prefix string
	HTTP *http.Client
}
type S3Store struct { /* unexported fields */ }
func NewS3(cfg S3Config) (*S3Store, error)
func S3FromEnv() (*S3Store, error)
func (s *S3Store) Get(ctx context.Context, key string) ([]byte, Version, error)
func (s *S3Store) Create(ctx context.Context, key string, body []byte) (Version, error)
func (s *S3Store) Update(ctx context.Context, key string, body []byte, v Version) (Version, error)
func (s *S3Store) Delete(ctx context.Context, key string) error
func (s *S3Store) List(ctx context.Context, prefix, cursor string, limit int) ([]string, string, error)

Update answers ErrNotFound, not ErrChanged, when the record was deleted, so a retry loop stops. Mutate may call fn more than once, so fn must have no side effects. S3FromEnv returns nil, nil when the app has no store. Delete is unconditional and succeeds on a missing key. List returns keys in order, and an empty next means the last page.

DirStore is for tests and local development. It is correct within one process only.

Read this page as markdown — exact, unstyled, and cheap for an agent to fetch.