Skip to content

Data Layer

Every storage backend implements Crate!T, which extends CrateAccess:

interface CrateAccess {
@safe:
IQuery get(); // Query all items
IQuery getItem(const string id); // Query single item by ID
Json addItem(const Json item); // Create item, return with generated ID
Json[] addItems(const Json[] items); // Create many, in as few round trips as the backend allows
Json updateItem(const Json item); // Replace item, return updated
void deleteItem(const string id); // Remove item
void deleteItems(const string[] ids); // Remove many by id
}
interface Crate(Type) : CrateAccess {
}

addItems and deleteItems exist so an import or a cascading delete is not N round trips. MongoCrate batches them into insertMany and deleteMany calls of up to 2000 documents each; MemoryCrate loops. They are semantically equivalent to calling the single-item method repeatedly — implement them that way in a custom crate if your backend has no bulk primitive.

Operations never call the storage backend directly — they go through Crate!T. This means you can swap MemoryCrate for MongoCrate (or your own implementation) without changing any routing or middleware code.

Stores items in a Json[] array. Auto-generates IDs as incrementing integers. Useful for testing and prototyping.

auto crate = new MemoryCrate!User;
crate.addItem(`{"name": "Alice"}`.parseJsonString);

Stores items in a MongoDB collection. Uses ObjectId for IDs and translates queries to BSON.

import vibe.db.mongodriver.mongo : connectMongoDB;
auto client = connectMongoDB("localhost");
auto crate = new MongoCrate!User(client, "mydb.users");

Crate uses vibe-mongodriver, not vibe-d:mongodb. Import connectMongoDB from vibe.db.mongodriver.mongo — a MongoCollection from the older vibe.db.mongo package is a different type and will not compile against MongoCrate.

byId and byIds build a query on the model’s declared id field, promoting whatever you hand them — string, Json, or ObjectId — to the type that field actually uses:

auto one = userCrate.byId(request.params["id"]);
auto many = userCrate.byIds(["6710...", "6711..."]);

oid is the UFCS shorthand for the same promotion in isolation: id.oid instead of ObjectId.fromString(id). An ObjectId that fails to parse serializes to BSON null rather than undefined, so it compares cleanly in a Mongo filter and matches nothing, instead of failing the query with “cannot compare to undefined”. Asking for the item id null on an ObjectId-keyed model raises CrateNotFoundException.

Both accept an optional CrateConfig!T to enable or disable individual CRUD operations:

struct CrateConfig(T) {
bool getList = true;
bool getItem = true;
bool addItem = true;
bool deleteItem = true;
bool replaceItem = true;
bool updateItem = true;
string singular = Singular!T; // Auto-derived from type name
string plural = Plural!T;
}

get() and getItem() return IQuery, a chainable query builder:

IQuery query = crate.get();
query
.where("status").equal("active")
.where("age").greaterThan(18)
.sort("name", 1)
.limit(10)
.skip(20);
auto results = query.exec(); // Returns InputRange!Json
auto count = query.size(); // Returns count without fetching

IFieldQuery supports operators like equal, greaterThan, lessThan, like, arrayContains, anyOf, and more. MemoryQuery evaluates these against the in-memory array; MongoQuery translates them to BSON queries.

When a client POSTs a Campaign that references a Team by ID:

{
"campaign": {
"title": "Save the Park",
"team": "abc123"
}
}

Crate needs to resolve "abc123" into a full Team object before storing it. This is what Lazy!T does.

Lazy!T wraps JSON data and generates proxy accessors for every field using compile-time introspection:

struct Lazy(T) {
enum Description = describeModel!T;
private {
Json __data = Json.emptyObject;
ResolveHandler __resolve; // Json delegate(string model, string id)
}
// Generated at compile time: one getter/setter per field
// Basic fields → direct JSON access
// Relation fields → lazy resolution via __resolve
}

For basic fields (strings, ints, enums), the proxy reads/writes directly from the internal JSON.

For relation fields (structs with an _id), the proxy calls the __resolve delegate to fetch the full object by ID. This delegate is wired to the global crateGetters registry.

In CreateItemApiOperation and UpdateItemApiOperation:

// 1. Wrap client JSON with a resolver
auto value = Lazy!Type(clientData, (&itemResolver).toDelegate);
// 2. Materialize to concrete type (triggers resolution of all relations)
value = Lazy!Type.fromModel(value.toType);
// 3. Convert back to storage-ready JSON (relations become IDs again)
result = crate.addItem(value.toJson);

The itemResolver function looks up the getter from the global registry:

Json itemResolver(string modelName, string id) {
if (auto getter = modelName in crateGetters) {
return (*getter)(id);
}
throw new Exception("No getter for " ~ modelName ~ " model");
}

This is why crateGetters registration is critical — without it, relation resolution fails at runtime.

A relation without @optional must be present: a missing one fails the request with CrateValidationException naming the field. A relation present but set to an empty object fails the same way, whether optional or not — an empty object is a malformed reference, not an absent one.

An @optional single relation that points at an item the resolver cannot find is dropped rather than raised: CrateNotFoundException and CrateRelationNotFoundException are swallowed for that field, and the rest of the item is stored. A dangling optional reference should not take the whole write down with it. Optional relation lists do not get this treatment — a missing list materializes as an empty array.

crate.lazydata.base publicly imports the whole set, so importing it is enough. The pieces are split so each wrapper is readable and testable on its own:

ModuleHolds
crate.lazydata.lazyModelLazy!T for a model struct — the generated field proxies and relation resolution
crate.lazydata.lazyJsonPass-through wrapper; no transformation
crate.lazydata.lazySysTimeConverts to and from the ISO extended string format
crate.lazydata.lazyArrayListList-shaped array fields
crate.lazydata.lazyArrayHashmapMap-shaped array fields
crate.lazydata.arrayImplThe shared array proxy implementation
crate.lazydata.fieldPer-field description used by the generated accessors
crate.lazydata.handlersThe resolver delegate types

Wrapper throughput is covered by a benchmark rather than an estimate:

Terminal window
dub run --config=benchmark --build=release

ModelDescription — Compile-Time Introspection

Section titled “ModelDescription — Compile-Time Introspection”

describeModel!T inspects a D struct at compile time and produces a ModelDescription:

struct ModelDescription {
string singular; // "campaign"
string plural; // "campaigns"
string source; // "campaigns" (collection/table name)
ModelFields fields;
string[] attributes; // UDAs attached to the type
}

Fields are categorized into three groups:

CategoryConditionExample
BasicPrimitives, strings, enums, Jsonstring name, int age, bool active
ObjectStruct without _id field (embedded)struct Address { string street; }
RelationStruct with _id field (reference)Team team where Team has _id
struct ModelFields {
FieldDescription[] basic;
ObjectFieldDescription[] objects; // Embedded structs
ModelFieldDescription[] relations; // Referenced models
}

Each field records its name, D type, whether it’s @optional, whether it’s an ID field, and any custom attributes.

ModelDescription is used everywhere:

  • Lazy!T uses it to generate proxy accessors for each field category
  • Policy rules use singular/plural to generate URL paths (/campaigns, /campaigns/:id)
  • Serializers use it to wrap responses in the correct key ({"campaign": {...}})
  • Validation uses it to check which fields are required vs optional
  • collectRequiredGetters!T uses the relations list to verify crateGetters registration at startup