storage

package module
v0.9.1 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 33 Imported by: 0

README

Storage: Typed Object Storage for Go

Go Version PkgGoDev Go Report Card License Coverage

This repository contains a typed object store for Go applications. It stores resource metadata and JSON documents in a database, and provides the pieces that usually end up scattered across an application: queries, links, optimistic updates, locks, change feeds, sequences, lifecycle state, validation, and blobs.

The goal here is to keep the storage layer small enough to use directly, while still covering the common cases.

  • Typed. Resources are regular Go structs with a storage.Meta field.
  • Multi-tenant. Every resource belongs to a tenant, namespace, kind, and generated ID.
  • Durable. SQLite and PostgreSQL drivers provide transactions, migrations, locks, changes, and sequences.
  • Queryable. Filter, sort, page, search, and count resources without writing SQL.
  • Composable. Links, state machines, validation, conversion helpers, and blobs are optional.
  • cgo-free SQLite. The SQLite driver uses ncruces/go-sqlite3.

Disclaimer: This is a new v0.1 package. The API is useful today, but it is still possible that some names or contracts will change.

Installation

Requires Go 1.25 or newer.

Install the root package and the driver you need:

go get github.com/kelindar/storage@v0.1.0
go get github.com/kelindar/storage/driver/sqlite
# or:
go get github.com/kelindar/storage/driver/pgsql

The drivers are separate Go modules and depend directly on github.com/kelindar/storage. Pin them to the version or revision used by your application.

If a module proxy reports a checksum conflict for v0.1.0 while the tag is being indexed, use the repository directly:

GOPROXY=direct go get github.com/kelindar/storage@v0.1.0
GOPROXY=direct go get github.com/kelindar/storage/driver/sqlite

Resources

The main entry in the package is storage.Object. It is a regular Go struct which embeds storage.Meta and declares its kind:

type Document struct {
	storage.Meta `kind:"document" json:",inline"`
	Title        string `json:"title"`
}

storage.Meta provides the Object interface:

type Object interface {
	URN() storage.URN
	Status() string
	Created() (string, time.Time)
	Updated() (string, time.Time)
}

The metadata contains:

  • ID, Kind, Tenant, and Namespace.
  • State.
  • CreatedBy, CreatedAt, UpdatedBy, and UpdatedAt.
  • ExpiresAt, stored as Unix nanoseconds.

A resource kind is read from the kind tag. storage.New[T] creates a new object with a generated ID. storage.NewByType, storage.KindOf, and storage.KindOfT are the reflection helpers for code which does not know the concrete type at compile time.

Before opening a driver, register every resource type that the database will store:

registry := storage.NewRegistry()
storage.MustRegister[*Document](registry)

db, err := sqlite.Open("storage.db", registry)
if err != nil {
	panic(err)
}
defer db.Close()

The driver creates one table for every registered kind during migration. This is why registration must happen first. If blobs are used, storage.Blob must be registered as well.

The registry also lets you enumerate registered types, resolve a kind, and inspect fields through Type.Field.

Options

storage.Options adds application metadata when a type is registered:

  • Icon, Title, Plural, and Sort describe the resource.
  • States adds lifecycle rules.
  • Actions lists permission names.
  • Workflows lists application workflows.

Actions and Workflows are metadata only. Storage does not authorize actions or execute workflows. DefaultActions is used when Actions is empty.

Actors

Attach the actor performing a mutation to the context:

ctx := storage.WithActor(context.Background(), "user:123")
created, err := storage.Insert(ctx, db, document)

storage.Actor(ctx) returns the actor, or storage.UnknownActor when none is set. storage.SystemActor is available for system work.

A complete example

Here is a small program which creates and searches a document:

package main

import (
	"context"
	"fmt"

	"github.com/kelindar/storage"
	"github.com/kelindar/storage/driver/sqlite"
)

type Document struct {
	storage.Meta `kind:"document" json:",inline"`
	Title        string `json:"title"`
}

func main() {
	registry := storage.NewRegistry()
	storage.MustRegister[*Document](registry)

	db, err := sqlite.Open("storage.db", registry)
	if err != nil {
		panic(err)
	}
	defer db.Close()

	ctx := context.Background()
	document, err := storage.Create[*Document](ctx, db, func(document *Document) error {
		document.Title = "Hello, storage"
		return nil
	}, "acme", "default")
	if err != nil {
		panic(err)
	}

	rows, err := storage.Search[*Document](ctx, db, storage.Query{
		Tenant: "acme",
		Limit:  20,
	})
	if err != nil {
		panic(err)
	}
	for document := range rows {
		fmt.Println(document.ID, document.Title)
	}
}

Drivers

SQLite

The SQLite driver is in github.com/kelindar/storage/driver/sqlite:

db, err := sqlite.Open("app.db", registry)
if err != nil {
	return err
}
defer db.Close()

testDB := sqlite.OpenEphemeral(registry)
defer testDB.Close()

sqlite.Open uses the cgo-free ncruces/go-sqlite3 driver. OpenEphemeral opens an in-memory database and is useful for tests.

SQLite uses FTS5 for Query.Match when it is available. If FTS5 is not available, matching falls back to a case-insensitive substring search.

PostgreSQL

The PostgreSQL driver is in github.com/kelindar/storage/driver/pgsql:

db, err := pgsql.Open("postgres://user:password@localhost/app?sslmode=disable", registry)
if err != nil {
	return err
}
defer db.Close()

pgsql.Open uses the pgx database/sql driver and owns the database handle. If the application already has a *sql.DB, use pgsql.New:

sqlDB, err := sql.Open("pgx", dsn)
if err != nil {
	return err
}

db, err := pgsql.New(sqlDB, registry)
if err != nil {
	return err
}
defer db.Close() // does not close sqlDB

pgsql.New does not close the supplied *sql.DB. PostgreSQL Query.Match performs case-insensitive substring matching over stored JSON text; it does not provide SQLite FTS ranking.

Both drivers auto-migrate their shared tables and registered resource tables. Their raw Upload method rejects blob content. Wrap a driver with storage.NewStore when blobs are needed.

A custom backend implements storage.Storage, which covers Close, Registry, Lock, CRUD, links, search, count, changes, uploads, and sequences. storage.Store decorates that backend with a storage.Files implementation.

CRUD

The generic helpers are the normal way to work with typed resources:

  • storage.Create constructs and inserts a resource.
  • storage.Insert assigns a missing ID, applies the default state, and inserts it.
  • storage.Update writes an existing resource with optimistic concurrency.
  • storage.Patch fetches, changes, and updates a resource.
  • storage.Upsert inserts a resource or patches the existing one after a conflict.
  • storage.Overwrite refreshes the stored version before updating, so it can overwrite changes made by another writer.
  • storage.Fetch, storage.Delete, storage.Search, and storage.Count do what their names suggest.

For example:

document, err := storage.New[*Document]("acme", "default")
if err != nil {
	return err
}
document.Title = "First version"

document, err = storage.Insert(ctx, db, document)
if err != nil {
	return err
}

document, err = storage.Patch(ctx, db, document.URN(), func(document *Document) error {
	document.Title = "Updated version"
	return nil
})

Updates compare the UpdatedAt value read from the database. A stale object returns storage.ErrConflict.

Patch retries a conflict up to ten times. Its callback may therefore run ten times, and must only mutate the supplied object. It must be safe to repeat and should not send mail, publish an event, or perform another external side effect. The same rule applies to the patch callback passed to Upsert.

Use the error helpers instead of matching strings:

if storage.IsNotFound(err) {
	// ...
}
if storage.IsConflict(err) {
	// refetch or retry
}
if storage.IsInvalidTransition(err) {
	// reject the requested state change
}
if storage.IsLockLost(err) {
	// stop work performed under the lease
}

The package also exposes ErrInvalid, ErrDeleting, and ErrKindNotFound for errors.Is checks.

URNs and targets

A storage.URN has the following format:

urn:tenant:namespace:kind:id

Use NewURN for a generated ID, MakeURN for a known ID, and ParseURN for an encoded value:

urn, err := storage.NewURN("acme", "default", "document")
if err != nil {
	return err
}

parsed, err := storage.ParseURN(urn.String())
if err != nil {
	return err
}
_ = parsed.IsValid()

Tenants, namespaces, and kinds are normalized to lowercase and must use the package's slug format. IDs are 20-character xid-compatible values. URNs marshal to and from JSON strings.

storage.Target adds a selector to a URN. Supported references are @draft, @latest, and exact positive versions such as @v3:

target, err := storage.ParseTarget(urn.String() + "@latest")
if err != nil {
	return err
}
fmt.Println(target.URN(), target.Ref(), target.IsLatest())

A target is a selector value. It does not create version storage by itself.

Queries

storage.Query lets you filter and sort a resource kind without writing SQL:

query := storage.Query{
	Tenant:     "acme",
	IDs:        []string{"id1", "id2"},
	Namespaces: []string{"default"},
	States:     []string{"active"},
	Indexes:    []string{"sample"},
	Filters: map[string][]string{
		"title":       {"release"},
		"description": {""}, // existence check
	},
	Match:        "deploy production",
	SortBy:       []string{"-updatedAt", "title"},
	Offset:       0,
	Limit:        50,
	UpdatedAfter: time.Now().Add(-time.Hour),
}

The query fields are:

  • Tenant, IDs, and Namespaces scope results.
  • States filters lifecycle states.
  • Indexes filters the value returned by an optional Index() string method.
  • Filters compare JSON paths.
  • Match performs full-text or substring matching, depending on the driver.
  • SortBy accepts +field for ascending and -field for descending order.
  • Offset and Limit page results.
  • CreatedBefore, UpdatedBefore, and UpdatedAfter apply time bounds.

A filter with a value is an equality check. A filter with the empty string is an existence check: the field must be present and not be empty, zero, or false. Nested fields can be addressed with paths such as profile.email.

Search returns an iterator. Collect drains it into a slice, and Select drains it while projecting another value:

rows, err := storage.Search[*Document](ctx, db, query)
if err != nil {
	return err
}

for document := range rows {
	fmt.Println(document.Title)
}

count, err := storage.Count[*Document](ctx, db, query)
if err != nil {
	return err
}

rows, _ = storage.Search[*Document](ctx, db, query)
titles := storage.Select(rows, func(document *Document) (string, bool) {
	return document.Title, document.Title != ""
})

Count does not accept sorting, offset, or limit.

String queries

ParseQuery accepts semicolon-separated key/value pairs:

query, err := storage.ParseQuery(
	"tenant=acme;namespace=default;state=active;"+
		"filter=title:release,description;"+
		"match={Title};sort=-updatedAt;limit=20;offset=0",
	document,
	storage.Query{},
)

Supported components are tenant, id, namespace, state, index, filter, match, sort, limit, offset, and updatedAfter. Multiple IDs, namespaces, states, and indexes are comma-separated. namespace=* removes the namespace restriction.

match can substitute fields from the supplied object with {FieldName}. Substitutions support strings, integers, floats, and booleans. Filters use field:value for equality or just field for existence. Query.String() produces a compact representation for logging and transport.

JSON and YAML

ToJSON serializes a resource. FromJSON reads the kind field, resolves the concrete type through the registry, and unmarshals it. The reader variants accept an io.Reader:

data, err := storage.ToJSON(document)
decoded, err := storage.FromJSON(registry, data)

decoded, err = storage.ReadJSON(registry, reader)
decoded, err = storage.FromYAML(registry, yamlData)
decoded, err = storage.ReadYAML(registry, reader)

UnmarshalYAML uses JSON struct tags. ReadFile reads a file when its data argument is nil and chooses JSON, YAML, or YML from the extension.

A store tag changes where a field is persisted without changing its public JSON shape:

type SecretDocument struct {
	storage.Meta `kind:"secret_document" json:",inline"`
	Token string `json:"-" store:"credentials.token"`
}

ToJSON writes Token at credentials.token, and FromJSON reads it from that path. store:"-" removes a field from the stored representation. Nested structs, slices, arrays, and maps are supported.

Use storage.Embed for polymorphic embedded resources. It stores an Object while using the registry and the embedded object's kind field to decode the concrete type.

A field tagged with link:"kind" produces dependency links to URNs or URN strings of that kind:

type Conversation struct {
	storage.Meta `kind:"conversation" json:",inline"`
	Attachments  []storage.URN `json:"attachments" link:"blob"`
}

The link walker supports pointers, nested structs, slices, arrays, and maps. Paths use JSON field names and include indexes or map keys, for example attachments.0. A field with json:"-" or link:"-" is ignored.

For links that are not represented by a single tagged field, implement storage.Linker:

func (b *Bundle) Links() ([]storage.Link, error) {
	return []storage.Link{
		storage.Own(b.URN(), target, storage.Path("resources.0")),
	}, nil
}

Use storage.Use for a dependency and storage.Own for exclusive ownership. Drivers rebuild outgoing links during inserts and updates. Call db.Link(ctx, source) to rebuild links explicitly, and db.Links(ctx, target) to list incoming links.

Link paths can be inspected with Path.String, Path.Label, Path.Index, Path.ID, and Path.Walk. Links must have valid URNs, a non-empty path, the matching source, and the same tenant on both sides. Ownership is unique per target; blobs cannot own resources, and only bundles may own non-blob resources.

Locks, changes, and sequences

Named locks

Storage.Lock acquires a renewable named lease:

lockCtx, release, err := db.Lock(ctx, "rebuild-index")
if err != nil {
	return err
}
defer release()

// Use lockCtx for work that must stop if the lease is lost.
if err := rebuild(lockCtx); err != nil {
	return err
}
if err := context.Cause(lockCtx); storage.IsLockLost(err) {
	return err
}

Only one live owner can hold a name. The returned context is canceled if renewal fails or ownership is lost. Always release the lease and use the returned context for protected work.

Durable changes

storage.Changes[T] starts a named, persistent consumer for one resource kind:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

worker := storage.Changes[*Document](
	ctx,
	db,
	"search-indexer",
	time.Time{},
	func(ctx context.Context, batch []storage.Change) error {
		for _, change := range batch {
			fmt.Println(change.Action, change.URN, change.At)
		}
		return nil
	},
)

if err := worker.Wait(); err != nil {
	return err
}

The consumer cursor is durable and keyed by consumer name and kind. Delivery is at least once. If the callback returns an error, the same batch is retried until it succeeds or the context is canceled. Batches are borrowed only for the callback, so do not retain or modify them. Actions are create, update, and delete.

The after time selects the starting point for a new consumer. The Store sweeper prunes change history older than seven days; pruned history is not replayed.

Sequences

storage.Next[T] advances a sequence named by the resource kind. db.Next(ctx, name) supports an arbitrary sequence name:

sequence, err := storage.Next[*Document](ctx, db)
named, err := db.Next(ctx, "invoice")

Sequences are durable and atomic and return uint32.

Blobs

Blobs keep binary bytes in a storage.Files backend and metadata in the resource database. Register the blob type before opening the driver:

storage.MustRegister[*storage.Blob](registry, storage.Options{
	States: state.Machine{
		"create": "* -> active",
		"delete": "active -> deleting",
	},
})

Wrap a driver with storage.NewStore:

backend, err := sqlite.Open("app.db", registry)
if err != nil {
	return err
}

files := &storage.Memory{} // zero-value in-memory filesystem
store := storage.NewStore(backend, files)
defer store.Close()

scope := storage.URN{Tenant: "acme", Namespace: "default"}
blob, err := store.Upload(ctx, scope, "text/plain", []byte("hello"))
if err != nil {
	return err
}

data, err := blob.Read(ctx)
if err != nil {
	return err
}
_, err = blob.WriteTo(ctx, destination)
_ = data

storage.Memory is intended for tests. A custom Files implementation must also implement fs.FS, Write(context.Context, string, []byte), and Delete(context.Context, string).

Blob content is immutable. Uploads:

  • Limit uncompressed data to storage.MaxSize (64 MiB).
  • Detect and validate the MIME type.
  • Record the original size, stored size, and SHA-256 digest.
  • Compress text, JSON, XML, YAML, TOML, and selected vendor formats with zstd.
  • Verify size, decompression, and SHA-256 on every read.

The persisted Blob.Compression value is either CompressionRaw or CompressionZstd. Updating a blob changes metadata only; it does not replace the bytes.

Raw drivers reject uploads because they have no file backend.

Blob deletion is two-phase. A referenced blob returns ErrConflict; otherwise it is first marked deleting, then its file and metadata are removed. If file deletion fails, the blob remains retryable in the deleting state. Store.Recover(ctx) takes the blob recovery lock and retries all deleting blobs.

Expiration

Set Meta.ExpiresAt to a Unix-nanosecond deadline:

document.ExpiresAt = time.Now().Add(time.Hour).UnixNano()
document, err = storage.Update(ctx, db, document)

Start the store sweeper with a deletion callback:

store.Start(ctx, func(ctx context.Context, urn storage.URN) error {
	_, err := store.Delete(ctx, urn)
	return err
})

Expiration is eventual. The sweeper runs at a randomized interval between one and two hours, scans expired resources in pages, and invokes the callback. Failed or blocked deletions are logged and retried by a later sweep. Store.Start also enables change-log retention cleanup. Store.Close stops the sweeper and closes the wrapped storage.

Lifecycle state

A state.Machine maps action names to edges:

states := state.Machine{
	"create":  "* -> draft",
	"publish": "draft -> active",
	"archive": "active -> inactive",
}

storage.MustRegister[*Document](registry, storage.Options{States: states})

The wildcard source supplies the default state. The generic storage.Insert and storage.Upsert helpers assign it when no state is set. The generic storage.Update and storage.Patch helpers reject invalid transitions with ErrInvalidTransition.

The state package provides shared state names (Creating, Active, Inactive, Deleting, and Failed) plus Machine.TryAction, Machine.CanTransition, Machine.Default, Machine.States, and Edge.Value.

Validation

github.com/kelindar/storage/validate validates nested structs, slices, arrays, maps, and pointers. Validation tags use is, not valid:

type Input struct {
	Email string `json:"email" is:"required,email"`
	Age   int    `json:"age" is:"min(18)"`
}

ok, err := validate.Struct(&Input{
	Email: "person@example.com",
	Age:   21,
})

Struct requires a non-nil pointer to a struct and returns (bool, error). A failure may be a validate.Errors collection. Each validate.Error includes the field name, validator, nested path, and message.

The package includes validators for required values, strings, lengths, character classes, numbers, ranges, URLs, email, IP addresses, UUIDs, hashes, dates, encodings, and common identifiers. Register a custom validator with validate.Register; its negated !name form is registered automatically.

Conversion helpers

github.com/kelindar/storage/convert contains small helpers:

  • TitleCase, Label, and SlugLabel create display labels.
  • Strings trims, removes empty values, deduplicates, and sorts.
  • Int and Float parse strings with defaults.
  • Int64, Uint64, and Float64 convert common Go and JSON values.
  • ScheduleLabel creates a readable label from common five-field cron expressions.
  • BuiltinID creates a stable tenant-specific 20-character ID.

Package layout

/                 core storage API, registry, objects, queries, links, blobs
/state             lifecycle state machines
/validate          struct validation
/convert           labels, conversions, schedules, stable IDs
/driver/sqlite     standalone cgo-free SQLite module
/driver/pgsql      standalone PostgreSQL module using pgx
/bench             standalone benchmark module
/internal/walk     private reflection and link walker

internal/walk is an implementation detail and is not part of the public API.

Benchmarks

The benchmark is a separate Go module under bench. It exercises resource creation, registry lookup, CRUD, search, count, changes, links, expiration scans, locks, sequences, blobs, and deletion.

Run it with:

(cd bench && go run .)

Development

Run the root tests and checks:

go test ./...
go test -race ./...
go vet ./...

The nested modules are tested separately:

(cd driver/sqlite && go test ./...)
(cd driver/pgsql && go test ./...)
(cd bench && go test ./...)

Contributing

Pull requests are welcome. Please keep changes focused and run the relevant tests before sending one.

License

Storage is licensed under the MIT License.

Documentation

Index

Constants

View Source
const (
	UnknownActor = "unknown"
	SystemActor  = "system"
)
View Source
const MaxSize = 64 << 20

MaxSize is the largest uncompressed Blob payload.

Variables

View Source
var (
	ErrNotFound          = errors.New("storage: document was not found")
	ErrConflict          = errors.New("storage: write conflict")
	ErrInvalidTransition = errors.New("storage: invalid state transition")
	ErrDeleting          = errors.New("storage: content is deleting")
	ErrInvalid           = errors.New("storage: invalid input")
	ErrLockLost          = errors.New("storage: lock ownership lost")
)
View Source
var DefaultActions = []string{
	"create", "read", "update", "delete", "search", "count", "run", "fail",
}

DefaultActions lists standard permission actions used when Options.Actions is empty.

View Source
var (
	ErrKindNotFound = errors.New("resource: kind not found")
)

Functions

func Actor added in v0.9.1

func Actor(ctx context.Context) string

Actor returns the audit identity carried by ctx, or UnknownActor.

func Changes added in v0.9.1

func Changes[T Object](ctx context.Context, db Storage, consumer string, after time.Time, handle func(context.Context, []Change) error) async.Awaiter

Changes starts bounded, durable changes for T with the named persistent consumer. Wait observes its terminal error. The callback is retried until it succeeds or ctx is canceled.

func Collect added in v0.9.1

func Collect[T Object](seq iter.Seq[T], where func(T) bool) []T

Collect drains a search iterator, keeping items for which where returns true. A nil where keeps every item. Use this before running nested queries on SQLite, which only allows one open cursor at a time.

func Count added in v0.9.1

func Count[T Object](ctx context.Context, db Storage, q Query) (int, error)

Count returns the number of records that match the specified query.

func Create added in v0.9.1

func Create[T Object](ctx context.Context, db Storage, constructor func(obj T) error, tenant, namespace string) (T, error)

Create creates a new resource and inserts it into the storage.

func Delete added in v0.9.1

func Delete[T Object](ctx context.Context, db Storage, urn URN) (T, error)

Delete deletes a resource from the storage and returns the deleted object.

func Fetch added in v0.9.1

func Fetch[T Object](ctx context.Context, db Storage, urn URN) (T, error)

Fetch attempts to find a specific document in the storage layer.

func Insert added in v0.9.1

func Insert[T Object](ctx context.Context, db Storage, v T) (T, error)

Insert inserts a new resource into the storage.

func IsConflict added in v0.9.1

func IsConflict(err error) bool

IsConflict returns true if the specified error is a conflict error.

func IsInvalidTransition added in v0.9.1

func IsInvalidTransition(err error) bool

IsInvalidTransition checks whether the error is a state transition error.

func IsLockLost added in v0.9.1

func IsLockLost(err error) bool

IsLockLost returns true if the specified error reports lost lock ownership.

func IsNotFound added in v0.9.1

func IsNotFound(err error) bool

IsNotFound returns true if the specified error is a not found error.

func New added in v0.9.1

func New[T Object](tenant, namespace string, funcs ...func(obj T) error) (T, error)

New creates a new instance of the specified resource kind.

func Next added in v0.9.1

func Next[T Object](ctx context.Context, db Storage) (uint32, error)

Next advances the sequence named by T's resource kind and returns its new value.

func Overwrite added in v0.9.1

func Overwrite[T Object](ctx context.Context, db Storage, v T) (T, error)

Overwrite updates using the latest stored version.

func Patch added in v0.9.1

func Patch[T Object](ctx context.Context, db Storage, urn URN, patch func(T) error) (T, error)

Patch fetches a resource, applies patch, and retries the update with a fresh version when a concurrent change wins, up to ten attempts. The patch callback may therefore run up to ten times and must only mutate the supplied object; it must be side-effect-free and safe to repeat.

func ReadFile added in v0.9.1

func ReadFile(path string, data []byte, v any) error

ReadFile decodes a file into v using its extension (.json, .yaml, .yml). When data is nil the file contents are read from path.

func Search[T Object](ctx context.Context, db Storage, q Query) (iter.Seq[T], error)

Search performs a query against the storage layer.

func Select added in v0.9.1

func Select[T Object, P any](seq iter.Seq[T], where func(T) (P, bool)) []P

Select drains a search iterator, projecting items for which where returns true.

func ToJSON added in v0.9.1

func ToJSON(v Object) ([]byte, error)

ToJSON encodes a resource for storage. Fields use their JSON name unless a store tag overrides it.

func UnmarshalYAML added in v0.9.1

func UnmarshalYAML(data []byte, v any) error

UnmarshalYAML decodes YAML into v using json struct tags.

func Update added in v0.9.1

func Update[T Object](ctx context.Context, db Storage, v T) (T, error)

Update updates an existing resource in the storage.

func Upsert added in v0.9.1

func Upsert[T Object](ctx context.Context, db Storage, v T, patch func(T) error) (T, error)

Upsert inserts a resource, or patches the existing resource with the same URN when insertion conflicts. The patch callback is not called when the insert succeeds and may run more than once when it patches an existing row.

func WithActor added in v0.9.1

func WithActor(ctx context.Context, actor string) context.Context

WithActor returns a context carrying the audit identity for storage mutations.

Types

type Blob added in v0.9.1

type Blob struct {
	Meta        `kind:"blob" json:",inline"`
	ContentType string      `json:"contentType" form:"ro"`
	Size        int64       `json:"size" form:"ro"`
	ObjectKey   string      `json:"-" store:"objectKey" form:"-"`
	SHA256      string      `json:"-" store:"sha256" form:"-"`
	StoredSize  int64       `json:"-" store:"storedSize" form:"-"`
	Compression Compression `json:"-" store:"compression" form:"-"`
	// contains filtered or unexported fields
}

Blob is immutable binary content stored outside the resource database. Storage details are retained only in the persisted representation.

func (*Blob) Read added in v0.9.1

func (b *Blob) Read(ctx context.Context) ([]byte, error)

Read returns verified original bytes.

func (*Blob) Subtitle added in v0.9.1

func (b *Blob) Subtitle() string

func (*Blob) Title added in v0.9.1

func (b *Blob) Title() string

func (*Blob) WriteTo added in v0.9.1

func (b *Blob) WriteTo(ctx context.Context, dst io.Writer) (int64, error)

WriteTo writes verified original bytes to dst.

type Change added in v0.9.1

type Change struct {
	URN    URN
	Action string
	At     time.Time
}

Change describes a durable create, update, or delete mutation. It is valid only for the duration of the Changes callback and must not be retained or modified by the consumer.

type Compression added in v0.9.1

type Compression string

Compression describes the persisted encoding of a Blob.

const (
	CompressionRaw  Compression = "raw"
	CompressionZstd Compression = "zstd"
)

type Embed added in v0.9.1

type Embed struct {
	Value    Object `json:",inline"`
	Registry Registry
}

Embedded represents a generic embedded document for unmarshaling

func (Embed) MarshalJSON added in v0.9.1

func (r Embed) MarshalJSON() ([]byte, error)

MarshalJSON marshals the JSON from the embedded document

func (*Embed) UnmarshalJSON added in v0.9.1

func (r *Embed) UnmarshalJSON(b []byte) error

UnmarshalJSON unmarshals the JSON into the embedded document

type Files added in v0.9.1

type Files interface {
	fs.FS
	Write(context.Context, string, []byte) (string, error)
	Delete(context.Context, string) error
}

Files is the object filesystem used for Blob content.

type Indexer added in v0.9.1

type Indexer interface {
	Index() string
}

Indexer represents a resource that provides an index.

type Kind added in v0.9.1

type Kind string

Kind represents a resource Kind (e.g. "Document", "Sprite")

const (
	// KindBlob identifies binary resources.
	KindBlob Kind = "blob"
)

func KindOf added in v0.9.1

func KindOf(typ reflect.Type) (Kind, error)

KindOf returns the Kind of the object.

func KindOfT added in v0.9.1

func KindOfT[T any]() (Kind, error)

KindOfT returns the Kind of the object.

func (Kind) String added in v0.9.1

func (k Kind) String() string

String returns the string representation of the resource kind.

type Link struct {
	Source URN      `json:"source"`
	Target URN      `json:"target"`
	Path   Path     `json:"path"`
	Kind   LinkKind `json:"kind"`
}

Link is a derived link index entry.

func Own added in v0.9.1

func Own(source, target URN, path Path) Link

Own returns an ownership link.

func Use added in v0.9.1

func Use(source, target URN, path Path) Link

Use returns a dependency link.

type LinkKind added in v0.9.1

type LinkKind uint8

LinkKind distinguishes exclusive ownership from an ordinary dependency.

const (
	LinkOwnership LinkKind = iota + 1
	LinkDependency
)

type Linker added in v0.9.1

type Linker interface {
	Links() ([]Link, error)
}

Linker is implemented by documents that declare explicit links.

type Memory added in v0.9.1

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

Memory is a zero-value in-memory object filesystem for tests.

func (*Memory) Delete added in v0.9.1

func (m *Memory) Delete(ctx context.Context, name string) error

func (*Memory) Open added in v0.9.1

func (m *Memory) Open(name string) (fs.File, error)

func (*Memory) Write added in v0.9.1

func (m *Memory) Write(ctx context.Context, name string, data []byte) (string, error)

type Meta added in v0.9.1

type Meta struct {
	ID        string `json:"id" form:"-"`                  // Globally unique identifier (e.g. "9m4e2mr0ui3e8a215n4g")
	Kind      Kind   `json:"kind" form:"-"`                // Meta kind (e.g. "deployment")
	Tenant    string `json:"tenant" form:"-"`              // Tenant slug (e.g. "acme")
	Namespace string `json:"namespace" form:"-"`           // Namespace of the object (e.g. "default")
	State     string `json:"state,omitempty"  form:"-"`    // State is the current state of the resource
	CreatedBy string `json:"createdBy,omitempty" form:"-"` // CreatedBy is the user who created the resource
	CreatedAt int64  `json:"createdAt,omitempty" form:"-"` // CreatedAt is the time when the resource was created
	UpdatedBy string `json:"updatedBy,omitempty" form:"-"` // UpdatedBy is the user who last updated the resource
	UpdatedAt int64  `json:"updatedAt,omitempty" form:"-"` // UpdatedAt is the time when the resource was last updated
	ExpiresAt int64  `json:"expiresAt,omitempty" form:"-"` // ExpiresAt is when the resource becomes eligible for deletion
}

Meta represents a metadata of the object.

func (*Meta) Created added in v0.9.1

func (r *Meta) Created() (string, time.Time)

Created returns who created the resource and when.

func (*Meta) Status added in v0.9.1

func (r *Meta) Status() string

Status returns the current state of the resource.

func (*Meta) Title added in v0.9.1

func (r *Meta) Title() string

Title returns the title of the resource.

func (*Meta) URN added in v0.9.1

func (r *Meta) URN() URN

URN returns the URN of the object.

func (*Meta) Updated added in v0.9.1

func (r *Meta) Updated() (string, time.Time)

Updated returns who updated the resource and when.

type Object

type Object interface {
	URN() URN                     // URN returns the uniform identifier of the object
	Status() string               // Status returns the current state
	Created() (string, time.Time) // Created returns createdBy and createdAt information
	Updated() (string, time.Time) // Updated returns updatedBy and updatedAt information
}

Object represents an object in the system.

func FromJSON added in v0.9.1

func FromJSON(c Registry, data []byte) (Object, error)

FromJSON parses a JSON file and returns a resource

func FromYAML added in v0.9.1

func FromYAML(c Registry, data []byte) (Object, error)

FromYAML parses a YAML document and returns a resource.

func NewByType added in v0.9.1

func NewByType(typ reflect.Type, tenant, namespace string) (Object, error)

New creates a new instance of the specified resource kind.

func ReadJSON added in v0.9.1

func ReadJSON(c Registry, reader io.Reader) (Object, error)

ReadJSON reads a JSON file and returns a resource

func ReadYAML added in v0.9.1

func ReadYAML(c Registry, reader io.Reader) (Object, error)

ReadYAML reads a YAML document and returns a resource.

type Options added in v0.9.1

type Options struct {
	Icon      string        `json:"icon,omitempty"`      // Icon name from https://lucide.dev/icons
	Title     string        `json:"title,omitempty"`     // Title of the document (e.g. Person)
	Plural    string        `json:"plural,omitempty"`    // Plural name of the document (e.g. People)
	Sort      string        `json:"sort,omitempty"`      // Sort field
	States    state.Machine `json:"-"`                   // Optional lifecycle state machine
	Actions   []string      `json:"actions,omitempty"`   // Allowed permission actions for this kind
	Workflows []string      `json:"workflows,omitempty"` // Built-in workflows to run after saves
}

Options represents the options for a document

type Path added in v0.9.1

type Path string

Path represents a rendering path for a particular field.

func (Path) ID added in v0.9.1

func (p Path) ID(prefix string) string

ID generates a unique ID for the path, encoded in hex.

func (Path) Index added in v0.9.1

func (p Path) Index() int

Index retrieves the index of the path, if it's a slice. Otherwise, returns -1.

func (Path) Label added in v0.9.1

func (p Path) Label() string

Label returns the label of the path.

func (Path) String added in v0.9.1

func (p Path) String() string

String returns the string representation of the path.

func (Path) Walk added in v0.9.1

func (p Path) Walk() iter.Seq[Path]

Walk iterates over all sub-paths (e.g. "foo.bar.baz" -> "foo", "foo.bar", "foo.bar.baz")

type Query added in v0.9.1

type Query struct {
	Tenant        string              // Tenant limits results to one tenant.
	IDs           []string            // IDs limits results to the listed resource IDs.
	Namespaces    []string            // Namespaces limits results to the listed namespaces.
	States        []string            // States is a list of states to filter by
	Indexes       []string            // Indexes is a list of indexes to filter by
	Filters       map[string][]string // Filters is a map of filters to apply
	Match         string              // Match is the full-text search query
	SortBy        []string            // Sort is the set of fields to order by
	Offset        int                 // Offset is the number of records to skip
	Limit         int                 // Limit is the maximum number of records to return
	CreatedBefore time.Time           // CreatedBefore filters records created before this time
	UpdatedBefore time.Time           // UpdatedBefore filters records updated before this time
	UpdatedAfter  time.Time           // UpdatedAfter filters records updated after this time
}

Query represents a query to filter records.

func ParseQuery added in v0.9.1

func ParseQuery(queryString string, object any, out Query) (Query, error)

ParseQuery parses a string query into a Query struct. The query format is structured as a semicolon-separated list of key-value pairs. Example query: "namespace=company;state=active;filter=age:30;match={Name}" - The query is limited to `company` namespace. - Only records with an `active` state will be considered. - A filter is applied to only include records where `age` is `30`. - It matches records containing the person's name from the placeholder `{Name}`.

  1. **namespace**: Specifies the namespaces to filter by. Multiple namespaces can be separated by commas. Example: `namespace=company,person`

  2. **state**: Indicates the states to filter by. Multiple states can be separated by commas. Example: `state=active,inactive`

  3. **filter**: Defines filters to apply. Each filter is specified as `field:value` for equality checks, or just `field` (without colon) for existence checks (non-nil and non-zero). Multiple filters can be separated by commas. Example: `filter=age:30,income:1000` (equality checks) Example: `filter=email` (existence check - matches records where email is set and non-empty)

  4. **match**: A full-text search query. This can include any search terms. Example: `match=software engineer`

func (*Query) String added in v0.9.1

func (q *Query) String() string

String returns the string representation of the query.

type Registry added in v0.9.1

type Registry interface {
	Types() iter.Seq[Type]
	Register(Type) error
	Resolve(Kind) (Type, error)
}

Registry represents a registry of various object kinds.

func NewRegistry added in v0.9.1

func NewRegistry() Registry

NewRegistry creates a new registry.

type Storage added in v0.9.1

type Storage interface {
	io.Closer
	Registry() Registry
	Lock(ctx context.Context, name string) (context.Context, context.CancelFunc, error)
	Insert(ctx context.Context, v Object) (Object, error)
	Update(ctx context.Context, v Object) (Object, error)
	Delete(ctx context.Context, urn URN) (Object, error)
	Fetch(ctx context.Context, urn URN) (Object, error)
	Link(ctx context.Context, source URN) error
	Links(ctx context.Context, target URN) ([]Link, error)
	Search(ctx context.Context, kind Kind, query Query) (iter.Seq[Object], error)
	Count(ctx context.Context, kind Kind, query Query) (int, error)
	Changes(ctx context.Context, consumer string, kind Kind, after time.Time, handle func(context.Context, []Change) error) error
	Upload(ctx context.Context, scope URN, contentType string, data []byte) (*Blob, error)
	Next(ctx context.Context, name string) (uint32, error)
}

Storage represents a storage layer for records.

type Store added in v0.9.1

type Store struct {
	Storage
	// contains filtered or unexported fields
}

Store joins resource storage with its file backend.

func NewStore added in v0.9.1

func NewStore(store Storage, files Files) *Store

NewStore joins resource storage with its file backend.

func (*Store) Close added in v0.9.1

func (s *Store) Close() error

Close stops background processes before closing storage.

func (*Store) Delete added in v0.9.1

func (s *Store) Delete(ctx context.Context, urn URN) (Object, error)

Delete removes ordinary resources directly. Deleting a Blob first makes it unreadable, then removes its file and metadata. File failures leave a retryable deleting resource.

func (*Store) Fetch added in v0.9.1

func (s *Store) Fetch(ctx context.Context, urn URN) (Object, error)

Fetch retrieves a resource and binds its file backend when needed.

func (*Store) Insert added in v0.9.1

func (s *Store) Insert(ctx context.Context, object Object) (Object, error)

Insert stores a resource and binds its file backend when needed.

func (*Store) Recover added in v0.9.1

func (s *Store) Recover(ctx context.Context) error

Recover removes files and metadata for Blobs left in the deleting state.

func (*Store) Search added in v0.9.1

func (s *Store) Search(ctx context.Context, kind Kind, query Query) (iter.Seq[Object], error)

Search retrieves resources and binds their file backend when needed.

func (*Store) Start added in v0.9.1

func (s *Store) Start(ctx context.Context, deleteResource func(context.Context, URN) error)

Start starts storage background processes.

func (*Store) Update added in v0.9.1

func (s *Store) Update(ctx context.Context, object Object) (Object, error)

Update stores a resource and binds its file backend when needed.

func (*Store) Upload added in v0.9.1

func (s *Store) Upload(ctx context.Context, scope URN, contentType string, data []byte) (*Blob, error)

Upload stores immutable content.

type Target added in v0.9.1

type Target string

Target identifies a resource and an optional reference.

func ParseTarget added in v0.9.1

func ParseTarget(raw string) (Target, error)

ParseTarget parses a target in the form <URN> or <URN>@<ref>.

func (Target) IsDraft added in v0.9.1

func (t Target) IsDraft() bool

IsDraft reports whether the target selects the draft version.

func (Target) IsLatest added in v0.9.1

func (t Target) IsLatest() bool

IsLatest reports whether the target selects the latest version.

func (Target) MarshalJSON added in v0.9.1

func (t Target) MarshalJSON() ([]byte, error)

MarshalJSON marshals a valid target as a JSON string.

func (Target) Ref added in v0.9.1

func (t Target) Ref() string

Ref returns the optional reference.

func (Target) String added in v0.9.1

func (t Target) String() string

String returns the target string.

func (Target) URN added in v0.9.1

func (t Target) URN() URN

URN returns the resource portion of the target.

func (*Target) UnmarshalJSON added in v0.9.1

func (t *Target) UnmarshalJSON(data []byte) error

UnmarshalJSON validates and unmarshals a target from a JSON string.

func (Target) Version added in v0.9.1

func (t Target) Version() int

Version returns the exact positive version, or zero for an unversioned selector.

type Type added in v0.9.1

type Type struct {
	Kind    Kind         // Kind of the resource
	Type    reflect.Type // Type of the resource
	Options              // Options of the resource
	// contains filtered or unexported fields
}

Type represents a registration of a resource kind.

func MustRegister added in v0.9.1

func MustRegister[T Object](c Registry, opts ...Options) Type

MustRegister registers a resource kind into the specified registry, panicking on error.

func Register added in v0.9.1

func Register[T Object](c Registry, opts ...Options) (Type, error)

Register registers a resource kind into the specified registry.

func (*Type) Field added in v0.9.1

func (t *Type) Field(path Path) (reflect.StructField, bool)

Field retrieves a field information by the specified path.

type URN added in v0.9.1

type URN struct {
	Tenant    string `json:"-" uri:"tenant" binding:"required"`    // Tenant slug (e.g. "acme")
	Namespace string `json:"-" uri:"namespace" binding:"required"` // Namespace name (e.g. "default")
	Kind      Kind   `json:"-" uri:"kind" binding:"required"`      // Object kind (e.g. "namespace")
	ID        string `json:"-" uri:"id"`                           // Globally unique identifier
}

URN represents a uniform resource name for accessing resources. Format: urn:tenant:namespace:kind:id

func MakeURN added in v0.9.1

func MakeURN(tenant, namespace string, kind Kind, id string) (URN, error)

MakeURN assembles a URN from its parts.

func NewURN added in v0.9.1

func NewURN(tenant, namespace string, kind Kind) (URN, error)

NewURN creates a new URN with a generated id.

func ParseURN added in v0.9.1

func ParseURN(s string) (URN, error)

ParseURN parses a string into a URN.

func (URN) IsValid added in v0.9.1

func (u URN) IsValid() bool

IsValid returns true if the URN is valid.

func (URN) MarshalJSON added in v0.9.1

func (u URN) MarshalJSON() ([]byte, error)

MarshalJSON marshals the URN to JSON.

func (URN) String added in v0.9.1

func (u URN) String() string

String returns the string representation of the URN.

func (*URN) UnmarshalJSON added in v0.9.1

func (u *URN) UnmarshalJSON(b []byte) error

UnmarshalJSON unmarshals the JSON into a URN.

Directories

Path Synopsis
driver
sqlite module
internal

Jump to

Keyboard shortcuts

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