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 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.
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.
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;}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.
Special Cases
Section titled “Special Cases”Lazy!SysTime— converts to/from ISO extended string formatLazy!Json— pass-through wrapper, no transformation- Array fields — uses
LazyArrayProxyfor 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}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