dsorm

package module
v0.0.0-...-abdc0c1 Latest Latest
Warning

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

Go to latest
Published: Mar 24, 2026 License: MIT Imports: 20 Imported by: 0

README

Run Tests

dsorm

dsorm is a high-performance Go ORM for Google Cloud Datastore with built-in caching support (Memory, Redis, Memcache). It extends the official client with lifecycle hooks, struct tags for keys, field encryption, a robust caching layer to minimize Datastore costs and latency, and an application-level cache utility with rate limiting and JSON helpers.

Features

  • Auto-Caching: Transparently caches keys/entities in Memory, Redis, or Memcache.
  • Model Hooks: BeforeSave, AfterSave, OnLoad, BeforeDelete, AfterDelete lifecycle methods.
  • Key Mapping: Use struct tags (e.g., model:"id") to map keys to struct fields.
  • Field Encryption: Built-in encryption for sensitive string fields via model:"name,encrypt" tag.
  • JSON Marshaling: Store complex structs/maps as compact JSON strings via model:"name,marshal" tag.
  • SQLite Backend: High-performance SQLite-backed store — build local/embedded apps or develop offline using the same datastore API.
  • QueryBuilder: Fluent query API with filters, ordering, pagination, ancestor, and namespace support.
  • API Parity: Wraps standard datastore methods (Put, Get, RunInTransaction) for easy migration.

Installation

go get github.com/altlimit/dsorm

Usage

1. Initialization

Initialize the client with your context. dsorm automatically detects the best cache backend:

  • App Engine: Uses Memcache.
  • Redis (REDIS_ADDR env): Uses Redis.
  • Default: Uses in-memory cache.
ctx := context.Background()

// Basic Init (Auto-detects)
client, err := dsorm.New(ctx)

// With Options
client, err = dsorm.New(ctx,
    dsorm.WithProjectID("my-project"),
    dsorm.WithEncryptionKey([]byte("my-32-byte-secret-key-here......")),
)

// With Local SQLite Store (same API, no cloud dependency)
// NewStore accepts a DIRECTORY path, not a database file path.
// It creates separate .db files per namespace inside this directory.
store := local.NewStore("/tmp/myapp")   // import "github.com/altlimit/dsorm/ds/local"
client, err = dsorm.New(ctx, dsorm.WithStore(store))
defer client.Close()  // Close when done (closes underlying store connections)
2. Defining Models

Embed dsorm.Base and use tags for keys, properties, and lifecycle management.

type User struct {
    dsorm.Base
    ID        string            `model:"id"`                              // Key Name (auto-excluded from datastore)
    Namespace string            `model:"ns"`                              // Key Namespace (auto-excluded)
    Parent    *datastore.Key    `model:"parent"`                          // Key Parent (auto-excluded)
    Username  string
    Email     string            `datastore:"email"`                       // Indexed property
    Bio       string            `datastore:"bio,noindex"`                 // Not indexed
    Secret    string            `model:"secret,encrypt"`                  // Encrypted + JSON-stored (auto-excluded)
    Profile   map[string]string `model:"profile,marshal" datastore:"-"`   // JSON-marshaled (datastore:"-" needed for maps)
    Tags      []string          `datastore:"tag"`                         // Multi-valued (each element indexed)
    CreatedAt time.Time         `model:"created"`                         // Auto-set on creation
    UpdatedAt time.Time         `model:"modified"`                        // Auto-set on every save
}
Tag Reference
Tag Purpose Example
model:"id" Maps field to key ID/Name (string or int64). Auto-excluded from datastore. ID string \model:"id"``
model:"parent" Maps to key parent. Auto-excluded. (*datastore.Key or *ParentModel) Parent *datastore.Key \model:"parent"``
model:"ns" Maps to key namespace. Auto-excluded. NS string \model:"ns"``
model:"id,store" Maps to key ID AND stores as datastore property ID string \model:"id,store"``
model:"created" Auto-set time.Time on first Put CreatedAt time.Time \model:"created"``
model:"modified" Auto-set time.Time on every Put UpdatedAt time.Time \model:"modified"``
model:"name,marshal" JSON-marshal into a property. Auto-excluded from SaveStruct. Data map[string]string \model:"data,marshal"``
model:"name,encrypt" JSON-marshal + AES encrypt. Auto-excluded. Secret string \model:"secret,encrypt"``
datastore:"name" Property name for Datastore Email string \datastore:"email"``
datastore:"-" Exclude from Datastore Temp string \datastore:"-"``
datastore:",noindex" Store without indexing Bio string \datastore:",noindex"``

Note: Fields tagged with model:"id", model:"ns", and model:"parent" are automatically excluded from datastore storage — no need for datastore:"-". Use ,store (e.g., model:"id,store") to opt-in. For model:"...,marshal" and model:"...,encrypt", auto-exclusion works for simple types (string, int64), but fields with types unsupported by datastore.SaveStruct (e.g., map, custom structs) still require datastore:"-".

3. CRUD Operations

You don't need to manually construct keys. Just set the ID field.

// Create
user := &User{ID: "alice", Username: "Alice"}
err := client.Put(ctx, user) // Key auto-constructed from ID

// Read
fetched := &User{ID: "alice"}
err := client.Get(ctx, fetched)

// Update
fetched.Username = "Alice_Updated"
err := client.Put(ctx, fetched) // UpdatedAt auto-updated

// Delete
err := client.Delete(ctx, fetched)

// Batch Operations
users := []*User{{ID: "a", Username: "A"}, {ID: "b", Username: "B"}}
err := client.PutMulti(ctx, users)
err = client.GetMulti(ctx, users)
err = client.DeleteMulti(ctx, users)
4. Queries

Use dsorm.NewQuery() with the fluent QueryBuilder API:

// Basic query
q := dsorm.NewQuery("User").FilterField("email", "=", "[email protected]")
users, nextCursor, err := dsorm.Query[*User](ctx, client, q, "")

// Filters, ordering, and pagination
q = dsorm.NewQuery("User").
    FilterField("score", ">=", 50).
    Order("score").
    Limit(10)
page1, cursor, err := dsorm.Query[*User](ctx, client, q, "")
page2, cursor, err := dsorm.Query[*User](ctx, client, q, cursor)

// Ancestor queries
parentKey := datastore.NameKey("Team", "engineering", nil)
q = dsorm.NewQuery("User").Ancestor(parentKey)

// Namespace queries
q = dsorm.NewQuery("User").Namespace("tenant-1")

// GetMulti by IDs (string, int64, *datastore.Key)
users, err := dsorm.GetMulti[*User](ctx, client, []string{"alice", "bob"})
QueryBuilder Methods
Method Description
FilterField(field, op, value) Add filter (=, >, >=, <, <=, in, not-in)
Order(field) Sort ascending; prefix with - for descending
Limit(n) Maximum results
Offset(n) Skip first N results
Ancestor(key) Scope to ancestor
Namespace(ns) Scope to namespace
Start(cursor) Resume from cursor
KeysOnly() Return only keys
5. Transactions
_, err := client.Transact(ctx, func(tx *dsorm.Transaction) error {
    user := &User{ID: "bob"}
    if err := tx.Get(user); err != nil {
        return err
    }
    user.Username = "Bob (Verified)"
    return tx.Put(user)
})

// Batch operations in transactions
_, err = client.Transact(ctx, func(tx *dsorm.Transaction) error {
    // tx.PutMulti, tx.GetMulti, tx.Delete, tx.DeleteMulti all available
    return tx.PutMulti(users)
})
6. Lifecycle Hooks

Implement any of these interfaces on your model:

func (u *User) BeforeSave(ctx context.Context, old dsorm.Model) error  { ... }
func (u *User) AfterSave(ctx context.Context, old dsorm.Model) error   { ... }
func (u *User) BeforeDelete(ctx context.Context) error                 { ... }
func (u *User) AfterDelete(ctx context.Context) error                  { ... }
func (u *User) OnLoad(ctx context.Context) error                       { ... }
7. Cache Utility

The cache package (github.com/altlimit/dsorm/cache) provides an application-level caching layer on top of any ds.Cache backend (Memory, Redis, Memcache). It simplifies single-key operations, adds atomic increment, typed JSON helpers, and built-in rate limiting.

import (
    dscache "github.com/altlimit/dsorm/cache"
    "github.com/altlimit/dsorm/cache/memory"
    "github.com/altlimit/dsorm/cache/redis"
)

// Wrap any ds.Cache backend
c := dscache.New(memory.NewCache())
// or
redisCache, _ := redis.NewCache("localhost:6379")
c = dscache.New(redisCache)

// Single-key operations
err := c.Set(ctx, "user:alice", []byte(`{"name":"Alice"}`), 5*time.Minute)
item, err := c.Get(ctx, "user:alice")  // returns *ds.Item or ds.ErrCacheMiss
err = c.Delete(ctx, "user:alice")

// Atomic increment (creates key if missing)
count, err := c.Increment(ctx, "page:views", 1, 24*time.Hour)

// Typed JSON helpers (generics)
type Profile struct { Name string; Score int }
err = dscache.Save(ctx, c, "profile:alice", Profile{Name: "Alice", Score: 42}, time.Hour)
profile, err := dscache.Load[Profile](ctx, c, "profile:alice")

// Rate limiting
result, err := c.RateLimit(ctx, "api:user:alice", 100, time.Minute)
if !result.Allowed {
    // result.Remaining == 0, result.ResetAt tells when the window resets
}

// Access the underlying ds.Cache for batch operations
raw := c.Unwrap()

Configuration

Variable Description
DATASTORE_PROJECT_ID Google Cloud Project ID
GOOGLE_CLOUD_PROJECT Fallback Project ID
DATASTORE_EMULATOR_HOST Local emulator address
DATASTORE_ENCRYPTION_KEY 32-byte key for field encryption (fallback)
REDIS_ADDR Address for Redis cache (e.g., localhost:6379)

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetMulti

func GetMulti[T Model](ctx context.Context, c *Client, ids any) ([]T, error)

GetMulti is a generic convenience wrapper around Client.GetMulti that accepts a slice of IDs (int, int64, string, or *datastore.Key) or model structs and returns a typed slice. Entities that do not exist in the datastore are set to nil in the returned slice.

func Query

func Query[T Model](ctx context.Context, c *Client, q *QueryBuilder, cursor string) ([]T, string, error)

Query is a generic convenience wrapper around Client.Query that returns a typed slice. It handles allocation and type assertion internally.

func WithEncryptionKeyContext

func WithEncryptionKeyContext(ctx context.Context, key []byte) context.Context

WithEncryptionKeyContext returns a copy of ctx carrying the given AES encryption key. This key takes priority over both WithEncryptionKey and the DATASTORE_ENCRYPTION_KEY environment variable when loading or saving fields tagged with model:"<name>,encrypt".

Use this when the encryption key must be determined at runtime on a per-request basis (e.g. derived from user credentials or a key-management service) rather than fixed at Client creation time.

Types

type AfterDelete

type AfterDelete interface {
	AfterDelete(context.Context) error
}

AfterDelete is an optional interface that a model can implement to run side-effect logic (e.g. cache invalidation, cascading deletes) after the entity has been successfully deleted from the datastore.

type AfterSave

type AfterSave interface {
	AfterSave(context.Context, Model) error
}

AfterSave is an optional interface that a model can implement to run side-effect logic after the entity has been successfully written to the datastore. The Model parameter contains the previously saved state (nil for new entities), enabling change-detection workflows such as audit logging or sending notifications.

type Base

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

Base provides a default implementation of the Model interface along with datastore.PropertyLoadSaver and datastore.KeyLoader. Embed this struct in your model to gain automatic key mapping, property marshaling/encryption, lifecycle hooks, and change tracking.

Fields are configured via the "model" struct tag:

model:"id"                  — maps the field as the entity's datastore key ID (string or int64)
model:"parent"              — maps the field as the entity's parent key
model:"ns"                  — maps the field as the entity's namespace
model:"created"             — auto-set to UTC now on first save
model:"modified"            — auto-set to UTC now on every save
model:"<name>,marshal"      — JSON-marshal the field into a single datastore property
model:"<name>,encrypt"      — JSON-marshal and AES-encrypt the field

func (*Base) IsNew

func (b *Base) IsNew() bool

IsNew reports whether the entity has not yet been loaded from the datastore. It returns true for freshly constructed structs that have never been Get'd.

func (*Base) Key

func (b *Base) Key() *datastore.Key

Key returns the datastore key for this entity. The key is populated automatically when the entity is loaded from the datastore or after a successful Put operation.

func (*Base) Load

func (b *Base) Load(ps []datastore.Property) error

Load implements datastore.PropertyLoadSaver.

func (*Base) LoadKey

func (b *Base) LoadKey(k *datastore.Key) error

LoadKey implements datastore.KeyLoader.

func (*Base) Save

func (b *Base) Save() ([]datastore.Property, error)

Save implements datastore.PropertyLoadSaver.

type BeforeDelete

type BeforeDelete interface {
	BeforeDelete(context.Context) error
}

BeforeDelete is an optional interface that a model can implement to run cleanup or validation logic before the entity is deleted from the datastore.

type BeforeSave

type BeforeSave interface {
	BeforeSave(context.Context, Model) error
}

BeforeSave is an optional interface that a model can implement to run validation or transformation logic before the entity is written to the datastore. The Model parameter contains the previously saved state of the entity (nil for new entities), allowing comparison between old and new values.

type Client

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

Client wraps datastore and cache operations.

func New

func New(ctx context.Context, opts ...Option) (*Client, error)

New creates a new Client with the given options.

Caching backend is auto-detected in the following order unless overridden with WithCache:

  1. App Engine Memcache (when running on App Engine)
  2. Redis (when REDIS_ADDR environment variable is set)
  3. In-memory cache (fallback)

func (*Client) Cache

func (c *Client) Cache() cache.Cache

Cache returns an application-level cache.Cache for general-purpose caching operations (Get, Set, Delete, Load, Save, RateLimit, etc).

func (*Client) Close

func (c *Client) Close() error

Close closes the underlying datastore and cache connections.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, val interface{}) error

Delete removes a single entity from the datastore. If val implements BeforeDelete, it is called before deletion. If val implements AfterDelete, it is called after successful deletion.

func (*Client) DeleteMulti

func (c *Client) DeleteMulti(ctx context.Context, vals interface{}) error

DeleteMulti removes multiple entities from the datastore. vals can be a slice of model structs or a []*datastore.Key. BeforeDelete and AfterDelete hooks are called for each entity that implements them.

func (*Client) Get

func (c *Client) Get(ctx context.Context, val interface{}) error

Get loads a single entity from the datastore into val. The entity's key is derived from val's struct tags (see Client.Key). After loading, the entity's Base.LoadKey is called to map the key back into the struct fields.

func (*Client) GetMulti

func (c *Client) GetMulti(ctx context.Context, vals interface{}) error

GetMulti loads multiple entities from the datastore. vals must be a slice of pointer-to-struct (e.g. []*MyModel). Entities that do not exist in the datastore are set to nil in the slice rather than returning an error. Only non-ErrNoSuchEntity errors are returned.

func (*Client) Key

func (c *Client) Key(val interface{}) *datastore.Key

Key returns the datastore key for a single model struct. The key is constructed from the struct's model:"id" (string or int64), model:"parent" (datastore.Key or parent model pointer), and model:"ns" (namespace) tags.

func (*Client) Keys

func (c *Client) Keys(val interface{}) []*datastore.Key

Keys returns a slice of datastore keys derived from a slice of model structs. Each entity's key is built from its model:"id", model:"parent", and model:"ns" struct tags.

func (*Client) Put

func (c *Client) Put(ctx context.Context, val interface{}) error

Put saves a single entity to the datastore. For new entities with an auto-generated int64 ID, the ID is written back into the struct after a successful save. If val implements BeforeSave, it is called before writing. If val implements AfterSave, it is called after writing with the previous state of the entity.

func (*Client) PutMulti

func (c *Client) PutMulti(ctx context.Context, vals interface{}) error

PutMulti saves multiple entities to the datastore. vals must be a slice of pointer-to-struct. Auto-generated IDs are written back into each struct. BeforeSave and AfterSave hooks are called for each entity that implements them. AfterSave hooks run concurrently.

func (*Client) Query

func (c *Client) Query(ctx context.Context, q *QueryBuilder, cursor string, vals interface{}) ([]*datastore.Key, string, error)

Query executes a query using a keys-only strategy: it first fetches all matching keys, then hydrates the entities via Client.GetMulti. If vals is non-nil, it must be a pointer to a slice (e.g. *[]*MyModel) and will be populated with the loaded entities. The returned string is the cursor for pagination; pass it back as the cursor argument to resume.

func (*Client) Store

func (c *Client) Store() ds.Store

Store returns the underlying ds.Store. To access backend-specific clients, type-assert to ds.CloudAccess or ds.LocalAccess:

if ca, ok := c.Store().(ds.CloudAccess); ok {
    raw := ca.DatastoreClient()
}
if la, ok := c.Store().(ds.LocalAccess); ok {
    sqlDB, err := la.DB("")
}

func (*Client) Transact

func (c *Client) Transact(ctx context.Context, f func(tx *Transaction) error) (*datastore.Commit, error)

Transact runs f inside a datastore transaction. If f returns nil, the transaction is committed and any pending auto-generated IDs are resolved back into their corresponding structs via Base.LoadKey. If f returns an error, the transaction is rolled back.

type Model

type Model interface {
	// IsNew reports whether the entity has not yet been loaded from the datastore.
	IsNew() bool
	// Key returns the datastore key for this entity, or nil if not yet assigned.
	Key() *datastore.Key
}

Model is the interface that all dsorm-managed structs must implement. Embed Base in your struct to get a default implementation.

type OnLoad

type OnLoad interface {
	OnLoad(context.Context) error
}

OnLoad is an optional interface that a model can implement to run custom logic immediately after the entity is loaded from the datastore. It is called after all properties have been deserialized and decrypted.

type Option

type Option func(*options)

Option configures a Client created via New.

func WithCache

func WithCache(c ds.Cache) Option

WithCache overrides the auto-detected caching backend with the provided implementation. By default, New selects App Engine Memcache, Redis (via REDIS_ADDR env), or in-memory cache, in that order.

func WithDatastoreClient

func WithDatastoreClient(c *datastore.Client) Option

WithDatastoreClient provides an existing cloud.google.com/go/datastore client instead of creating one internally. Useful for environments that require custom client configuration (e.g. emulator endpoints).

func WithEncryptionKey

func WithEncryptionKey(key []byte) Option

WithEncryptionKey sets the AES encryption key used for fields tagged with model:"<name>,encrypt". If not set, the DATASTORE_ENCRYPTION_KEY environment variable is used as a fallback at load/save time.

func WithNoCache

func WithNoCache() Option

WithNoCache disables the caching layer entirely. All reads and writes go directly to the datastore with no intermediate cache. This is useful for distributed systems where no shared caching backend (Redis, Memcache) is available and the default in-memory cache would cause stale reads across instances.

The same effect can be achieved by setting the DSORM_NO_CACHE environment variable to "1" or "true".

func WithProjectID

func WithProjectID(id string) Option

WithProjectID sets the Google Cloud project ID. If not specified, the DATASTORE_PROJECT_ID or GOOGLE_CLOUD_PROJECT environment variables are used as fallbacks.

func WithStore

func WithStore(s ds.Store) Option

WithStore replaces the default Google Cloud Datastore backend with a custom ds.Store implementation (e.g. the local in-memory store for testing).

type QueryBuilder

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

QueryBuilder represents a common query interface for dsorm.

func NewQuery

func NewQuery(kind string) *QueryBuilder

NewQuery creates a new query for a specific kind.

func (*QueryBuilder) Ancestor

func (q *QueryBuilder) Ancestor(ancestor *datastore.Key) *QueryBuilder

Ancestor sets the ancestor datastore key to queries.

func (*QueryBuilder) FilterField

func (q *QueryBuilder) FilterField(fieldName, operator string, value interface{}) *QueryBuilder

FilterField adds a field-specific filter to the query.

func (*QueryBuilder) Filters

func (q *QueryBuilder) Filters() []ds.Filter

func (*QueryBuilder) GetAncestor

func (q *QueryBuilder) GetAncestor() *datastore.Key

func (*QueryBuilder) GetCursor

func (q *QueryBuilder) GetCursor() string

func (*QueryBuilder) GetLimit

func (q *QueryBuilder) GetLimit() int

func (*QueryBuilder) GetNamespace

func (q *QueryBuilder) GetNamespace() string

func (*QueryBuilder) GetOffset

func (q *QueryBuilder) GetOffset() int

func (*QueryBuilder) IsKeysOnly

func (q *QueryBuilder) IsKeysOnly() bool

func (*QueryBuilder) KeysOnly

func (q *QueryBuilder) KeysOnly() *QueryBuilder

KeysOnly makes the query return only keys.

func (*QueryBuilder) Kind

func (q *QueryBuilder) Kind() string

Data Getters for drivers

func (*QueryBuilder) Limit

func (q *QueryBuilder) Limit(limit int) *QueryBuilder

Limit sets the maximum number of items to return.

func (*QueryBuilder) Namespace

func (q *QueryBuilder) Namespace(ns string) *QueryBuilder

Namespace sets the namespace for the query.

func (*QueryBuilder) Offset

func (q *QueryBuilder) Offset(offset int) *QueryBuilder

Offset sets the number of items to skip.

func (*QueryBuilder) Order

func (q *QueryBuilder) Order(fieldName string) *QueryBuilder

Order adds an order to the query.

func (*QueryBuilder) Orders

func (q *QueryBuilder) Orders() []ds.Order

func (*QueryBuilder) Start

func (q *QueryBuilder) Start(cursor string) *QueryBuilder

Start sets the cursor string where the query will begin.

type Transaction

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

Transaction wraps a datastore transaction with dsorm functionality including automatic key mapping, lifecycle hooks, and pending key resolution for auto-ID entities.

func (*Transaction) Delete

func (t *Transaction) Delete(val interface{}) error

Delete removes a single entity within the transaction.

func (*Transaction) DeleteMulti

func (t *Transaction) DeleteMulti(vals interface{}) error

DeleteMulti removes multiple entities within the transaction.

func (*Transaction) Get

func (t *Transaction) Get(val interface{}) error

Get loads a single entity within the transaction. Behaves like Client.Get but operates within the transaction's isolation.

func (*Transaction) GetMulti

func (t *Transaction) GetMulti(vals interface{}) error

GetMulti loads multiple entities within the transaction. Behaves like Client.GetMulti: entities that do not exist are set to nil in the slice.

func (*Transaction) Put

func (t *Transaction) Put(val interface{}) error

Put saves a single entity within the transaction. For new entities with auto-generated IDs, the ID is resolved after Client.Transact commits successfully.

func (*Transaction) PutMulti

func (t *Transaction) PutMulti(vals interface{}) error

PutMulti saves multiple entities within the transaction. Auto-generated IDs are resolved after Client.Transact commits.

Directories

Path Synopsis
Package cache provides application-level caching utilities built on top of the dsorm cache backends (Redis, memory, memcache).
Package cache provides application-level caching utilities built on top of the dsorm cache backends (Redis, memory, memcache).
memory
Package memory provides an in-memory cache implementation backed by an LRU eviction policy with per-item TTL support.
Package memory provides an in-memory cache implementation backed by an LRU eviction policy with per-item TTL support.
ds
internal

Jump to

Keyboard shortcuts

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