Data Layer
The Crate!T Interface
Section titled “The Crate!T Interface”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.
Built-In Implementations
Section titled “Built-In Implementations”MemoryCrate
Section titled “MemoryCrate”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);MongoCrate
Section titled “MongoCrate”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.
Looking Items Up by Id
Section titled “Looking Items Up by Id”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;}The Query Builder
Section titled “The Query Builder”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!Jsonauto count = query.size(); // Returns count without fetchingIFieldQuery 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.
Lazy!T — Deferred Relation Resolution
Section titled “Lazy!T — Deferred Relation Resolution”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.
How It Works
Section titled “How It Works”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.
The Resolution Flow
Section titled “The Resolution Flow”In CreateItemApiOperation and UpdateItemApiOperation:
// 1. Wrap client JSON with a resolverauto 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.
Optional Relations
Section titled “Optional Relations”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.
Module Layout
Section titled “Module Layout”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:
| Module | Holds |
|---|---|
crate.lazydata.lazyModel | Lazy!T for a model struct — the generated field proxies and relation resolution |
crate.lazydata.lazyJson | Pass-through wrapper; no transformation |
crate.lazydata.lazySysTime | Converts to and from the ISO extended string format |
crate.lazydata.lazyArrayList | List-shaped array fields |
crate.lazydata.lazyArrayHashmap | Map-shaped array fields |
crate.lazydata.arrayImpl | The shared array proxy implementation |
crate.lazydata.field | Per-field description used by the generated accessors |
crate.lazydata.handlers | The resolver delegate types |
Wrapper throughput is covered by a benchmark rather than an estimate:
dub run --config=benchmark --build=releaseModelDescription — 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}Field Classification
Section titled “Field Classification”Fields are categorized into three groups:
| Category | Condition | Example |
|---|---|---|
| Basic | Primitives, strings, enums, Json | string name, int age, bool active |
| Object | Struct without _id field (embedded) | struct Address { string street; } |
| Relation | Struct 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.
How It Drives the Framework
Section titled “How It Drives the Framework”ModelDescription is used everywhere:
- Lazy!T uses it to generate proxy accessors for each field category
- Policy rules use
singular/pluralto 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!Tuses the relations list to verifycrateGettersregistration at startup