Skip to main content

Database Models

Radish generates Mongoose models for each entity defined in your blueprints.

Overview​

Models provide the database layer abstraction, handling:

  • Schema definitions
  • Indexes
  • Timestamps (createdAt, updatedAt)
  • Owner scoping
  • Versioning hooks (if enabled)

Generated Models​

Location: .radish/lib/datalayer/core/models/

// .radish/lib/datalayer/core/models/product.model.ts
export const ProductModel = mongoose.model('Product', productSchema);

Features​

Default Collation​

All models are created with a default collation for case-insensitive, natural number sorting:

{ locale: 'en', strength: 2, numericOrdering: true }

This means string comparisons and sorting are case-insensitive by default, and numeric strings sort naturally (1, 2, 10 instead of 1, 10, 2). You can override this per-entity in your blueprint with a custom collation object.

Automatic Indexes​

Models include automatic indexes on:

  • Reference fields (ref fields with objectId type)
  • Fields listed in the entity's filters array
  • Compound indexes defined in the entity's indexes array
  • ownerId (always indexed)
  • archivedAt (always indexed)

Note: Setting index: true on a field in your blueprint does NOT create an actual MongoDB index. Only ref fields, filters entries, and explicit indexes definitions produce indexes. Use the indexes array for fields that need indexing beyond what is auto-generated.

Timestamps​

All models include createdAt and updatedAt timestamps.

Owner Scoping​

Models support owner-based access control when configured in blueprints.

Versioning​

When versioning is enabled, models include hooks to track changes.

Relations​

Forward references​

A field with a ref becomes a Mongoose reference, populatable by name:

"Lesson": { "fields": { "courseId": { "type": "objectId", "ref": "Course" } } }
await lessonService.get(id, { paths: ['courseId'] });   // resolves the Course

Inverse relations (reverse populate)​

The reverse direction is generated automatically. Course gets a lessons accessor without declaring anything:

await courseService.get(id, { paths: ['lessons'] });    // resolves its Lessons

Emitted as a Mongoose virtual, declared before the model is created:

CourseSchema.virtual("lessons", {
ref: "Lesson", localField: "_id", foreignField: "courseId", justOne: false
});

Everything is derived — there is no blueprint key.

from
where the accessor goesthe ref target
its namethe referencing entity's plural, camelCased to a valid identifier
one or manywhether the referencing field carries a unique index

A unique index means one record can point back, so the accessor is a single document and its name is singularised:

"Profile": { "fields": { "userId": { "type": "objectId", "ref": "User", "unique": true } } }
await userService.get(id, { paths: ['profile'] });      // one Profile, not an array

Without the unique index the same relation yields User.profiles as an array. Cardinality follows the index, so adding or removing one changes the accessor — worth knowing if a name changes unexpectedly after a schema edit.

Which entities get them​

All of them. Builtin (User.apiKeys, User.grants), business, system-owned (ScheduledJob.jobExecutions), feature-synthesised (DatasetDefinition.datasetFields), and entities extending a builtin — where refs inherited from the parent count, not just the entity's own fields.

An entity nothing references simply gets none.

Collisions​

An accessor that would collide with a real field on the target, or with another inverse claiming the same name, is skipped with a warning naming both sides. It is not silently dropped, because an inverse that exists on one model and not another for invisible reasons is worse than one that is missing loudly.

Two things worth knowing​

Virtuals are lazy. An inverse costs nothing until something populates it. Declaring many is not itself a performance concern — populating a large one on a hot path is.

.lean() drops virtuals. The repo layer does not use it, so this does not arise today. Any code path that adds it will find every inverse silently undefined — no error, just an absent field.