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 updateItem(const Json item); // Replace item, return updated
void deleteItem(const string id); // Remove item
}
interface Crate(Type) : CrateAccess {
}

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.

auto client = connectMongoDB("localhost");
auto crate = new MongoCrate!User(client, "mydb.users");

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.

  • Lazy!SysTime — converts to/from ISO extended string format
  • Lazy!Json — pass-through wrapper, no transformation
  • Array fields — uses LazyArrayProxy for lazy element access

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