Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Manifest Reference

Everything in Qleany is defined in qleany.yaml. This document covers all manifest options. The UI is still the primary way to create and edit manifests, but this reference helps when you need to edit the file directly. Or if you are curious.

Example Manifests

Real-world manifests you can reference:

ProjectLanguageFrontendLink
SkribistoC++20 / Qt6QtQuickqleany.yaml
QleanyRust 2024Slint + CLIqleany.yaml

Basic Structure

schema:
  version: 2

global:
  language: cpp-qt          # rust, cpp-qt
  application_name: MyApp
  organisation:
    name: myorg
    domain: myorg.com
  prefix_path: src

entities:
  - name: EntityBase
    # ...

features:
  - name: my_feature
    # ...

Required Base Entity

All entities must have id, created_at, and updated_at fields. These are essential for identity, caching, and change tracking.

To simplify this, Qleany provides EntityBase — a heritable entity with these three fields pre-defined. When you create a new manifest, Qleany automatically generates:

  • EntityBase with id, created_at, updated_at
  • An empty Root entity inheriting from EntityBase

All your entities should inherit from EntityBase using the inherits_from field.

entities:
  # EntityBase provides the necessary id, created_at, updated_at
  - name: EntityBase
    only_for_heritage: true
    allow_direct_access: false
    fields:
      - name: id
        type: uinteger
      - name: created_at
        type: datetime
      - name: updated_at
        type: datetime
        
  - name: Root
    inherits_from: EntityBase
    undoable: false
    fields:
      # Your root-level relationships here

Complete Example

schema:
  version: 2

global:
  language: cpp-qt
  application_name: MyApp
  organisation:
    name: myorg
    domain: myorg.com
  prefix_path: src

entities:
  - name: EntityBase
    only_for_heritage: true
    allow_direct_access: false
    fields:
      - name: id
        type: uinteger
      - name: created_at
        type: datetime
      - name: updated_at
        type: datetime
        
  - name: Root
    inherits_from: EntityBase
    undoable: false
    fields:
      - name: works
        type: entity
        entity: Work
        relationship: ordered_one_to_many
        strong: true
        list_model: true
        list_model_displayed_field: title

  - name: Work
    inherits_from: EntityBase
    undoable: true
    single_model: true
    fields:
      - name: title
        type: string
      - name: binders
        type: entity
        entity: Binder
        relationship: ordered_one_to_many
        strong: true
        list_model: true
        list_model_displayed_field: name
      - name: tags
        type: entity
        entity: BinderTag
        relationship: one_to_many
        strong: true

  - name: Binder
    inherits_from: EntityBase
    undoable: true
    fields:
      - name: name
        type: string
      - name: items
        type: entity
        entity: BinderItem
        relationship: ordered_one_to_many
        strong: true
        list_model: true
        list_model_displayed_field: title

  - name: BinderItem
    inherits_from: EntityBase
    undoable: true
    fields:
      - name: title
        type: string
      - name: parentItem
        type: entity
        entity: BinderItem
        relationship: many_to_one
      - name: tags
        type: entity
        entity: BinderTag
        relationship: many_to_many

features:
  - name: work_management
    use_cases:
      - name: load_work
        undoable: false
        entities: [Root, Work, Binder, BinderItem]
        dto_in:
          name: LoadWorkDto
          fields:
            - name: file_path
              type: string
        dto_out:
          name: LoadWorkReturnDto
          fields:
            - name: work_id
              type: integer

Entity Options

OptionTypeDefaultDescription
namestringrequiredEntity name (PascalCase)
inherits_fromstringnoneParent entity for inheritance
only_for_heritageboolfalseEntity used only as base class
undoableboolfalseEnable undo/redo for this entity’s controller
allow_direct_accessbooltrueGenerate files in direct_access/ for UI access
single_modelboolfalseGenerate Single{Entity} QML wrapper (C++/Qt only)

Field options

OptionTypeDefaultDescription
namestringrequiredField name (snake_case)
typestringrequiredField type (see below)
entitystringnoneFor entity type, name of the entity
relationshipstringnoneFor entity type, relationship type (see below)
optionalboolfalseFor one_to_one and many_to_one
strongboolfalseFor one_to_one, one_to_many, and ordered_one_to_many, enable cascade deletion
list_modelboolfalseFor C++/Qt only, generate a C++ QAbstractListModel and its QML wrapper for this relationship field
list_model_displayed_fieldstringnoneFor C++/Qt only, default display role for the generated ListModel
enum_namestringnoneFor enum type, name of the enum (PascalCase)
enum_valuesarraynoneFor enum type, list of enum values (PascalCase)

Field Types

TypeDescriptionExample
booleanTrue/false valueis_active: true
integerWhole numbercount: 42
floatDecimal numberprice: 19.99
stringTextname: "Alice"
uuidUnique identifierid: "550e8400-..."
datetimeDate and timecreated_at: "2024-01-15T10:30:00"
entityRelationship to another entitySee relationship section
enumEnumerated valueSee enum section

Enum Fields

- name: status
  type: enum
  enum_name: CarStatus
  enum_values:
    - Available
    - Reserved
    - Sold

Like entities, the enum name should be PascalCase. Enum values should also be PascalCase. And the name must be unique.


Relationship Fields

This section will seem to be a bit repetitive, but for those not familiar with database relationships, it’s important to get all the details right. And some people have different ways of understanding relationships, so I want to be as clear as possible. Bear with me.

When type: entity, additional options define the relationship:

Relationship Types

RelationshipJunction TypeReturn Type (C++ / Rust)
one_to_oneOneToOnestd::optional<int> / Option<i64>
many_to_oneManyToOnestd::optional<int> / Option<i64>
one_to_manyUnorderedOneToManyQList<int> / Vec<i64>
ordered_one_to_manyOrderedOneToManyQList<int> / Vec<i64>
many_to_manyUnorderedManyToManyQList<int> / Vec<i64>

Relationship Flags

FlagValid forEffect
optionalone_to_one, many_to_oneValidated on create/update (0..1 instead of 1..1)
strongone_to_one, one_to_many, ordered_one_to_manyCascade deletion — removing parent removes children

QML Generation Flags (C++/Qt only)

FlagEffect
list_modelGenerate {Entity}ListModelFrom{Parent}{Relationship}
list_model_displayed_fieldDefault display role for the list model

Validation Rules

Flagone_to_onemany_to_oneone_to_manyordered_one_to_manymany_to_many
optional✓/✗✓/✗N.A.N.A.N.A.
strong✓/✗ (see note)N.A.✓/✗✓/✗N.A.

N.A.: Not applicable for this relationship type. There will be no change in generated code, or don’t use them. When you write code, an empty list expresses the intent to hold no relationship.

Note: If one_to_one holds a weak relationship (strong: false), it couldn’t be required (so, it must be optional: true). There is the risk of a dangling reference if the entity targeted by the reference is deleted.

Some invalid combinations are rejected at manifest parsing. This validation step is still in the works.


Understanding Relationships

Database relationships describe how entities connect. Two concepts matter:

Cardinality — How many entities can participate on each side?

  • One — At most one entity (0..1 or exactly 1)
  • Many — Zero or more entities (0..*)

Direction — Which side “owns” the relationship?

  • The parent side holds the list of children
  • The child side holds a reference back to its parent (in Qleany case, this reference is hidden from the user)

The reality in Qleany is a bit more nuanced, but this mental model helps understand how to model your data.

┌─────────────────────────────────────────────────────────────┐
│                     RELATIONSHIP TYPES                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ONE-TO-ONE (1:1)                                          │
│   ┌───────┐         ┌───────┐                               │
│   │ User  │─────────│Profile│   Each user has one profile   │
│   └───────┘         └───────┘   Each profile belongs to     │
│                                 one user                    │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ONE-TO-MANY (1:N)                                         │
│   ┌───────┐         ┌───────┐                               │
│   │Group  │────────<│ Item  │   One group  has many items   │
│   └───────┘         └───────┘   Binder.items: [1, 2, 3]     │
│                                                             │
│   MANY-TO-ONE (N:1)                                         │
│   ┌───────┐         ┌───────┐                               │
│   │ Car   │>────────│Brand  │   Many items belong to one    │
│   └───────┘         └───────┘   brand.  Car.brand: 5        │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   MANY-TO-MANY (N:M)                                        │
│   ┌───────┐         ┌───────┐                               │
│   │ Item  │>───────<│  Tag  │   Items have many tags        │
│   └───────┘         └───────┘   Tags apply to many items    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Reality in Qleany

Like said earlier, the reality is a bit more nuanced:

  • Special junction tables are used for all relationships (even the simpler ones) and “sit” between parent and child tables
  • These junction tables can be accessed by parent and child tables equally.
  • This means that for every relationship, both sides can see each other (no true “back-reference” concept)
  • The relationship type defines how the junction table behaves, and how the parent and child entities see each other.
  • In the deeper code, there is always the mentions of a left entity and a right entity (parent and child respectively in the mental model).

This may be easier to understand: all relationships are defined from the perspective of the entity holding the field. This means that:

  • For one_to_one, the entity with the field is one side, the referenced entity is the other side. Car.brand: EntityId
  • For many_to_one, the entity with the field is the “many” side (several entities of the same type), the referenced entity is the “one” side. Car.brand: EntityId
  • For one_to_many and ordered_one_to_many, the entity with the field is the “one” side, the referenced entity is the “many” side. Car.brand: Vec<EntityId>
  • For many_to_many, both sides are “many”. Car.brand: Vec<EntityId>

Why this design? The manifest describes meaning: ownership, cardinality, ordering, whether deletion cascades. The generated code handles implementation: junction tables, column schemas, query patterns, cache invalidation. You think about your domain; the generator thinks about the database. These are deliberately separated.

The cost is storage overhead. A one_to_one that could be a single foreign key column gets its own junction table instead. For a desktop or mobile application with hundreds or thousands of entities, you will never notice. For a web backend serving millions of rows, you would care, but that’s not what Qleany targets.

The payoff is uniformity. Every relationship, regardless of type, goes through the same junction table infrastructure. This means one code path for snapshot/restore (which is what makes undo/redo work on entity trees), one code path for bidirectional navigation (both sides can always see each other), and one code path for the generator to produce. No special cases, no “this relationship is simple enough for a foreign key but that one needs a junction table.” The complexity stays in the infrastructure, not in your head.

Yes, database engineers might cringe at this, but this greatly simplifies the code generation and the overall mental model when designing your entities. They can cringe more when I say there is no notion of foreign keys in Qleany internal database.

When to use each:

RelationshipUse when…Example
one_to_oneExactly one related entity, exclusiveUser → Profile
many_to_oneMany entities reference one childCar → Brand, Comment → Post
one_to_manyParent owns a collection of childrenBinder → Items, Post → Comments
ordered_one_to_manySame as above, but order mattersBook → Chapters, Playlist → Songs
many_to_manyEntities share references both waysItems ↔ Tags, Students ↔ Courses

There is no ordered_many_to_many because I’m not mad enough to handle that complexity.

Relationship Examples

# Exclusive single reference (0..1) — each side has at most one. UserProfile is owned.
- name: profile
  type: entity
  entity: UserProfile
  relationship: one_to_one
  strong: true
  optional: true

# Back-reference to parent (N:1) — many children point to one parent
- name: parentItem
  type: entity
  entity: BinderItem
  relationship: many_to_one

# Required back-reference
- name: binder
  type: entity
  entity: Binder
  relationship: many_to_one
  # implied:
  optional: false

# Unordered children with cascade delete (1:N)
- name: tags
  type: entity
  entity: BinderTag
  relationship: one_to_many
  strong: true

# Ordered children (1:N with order)
- name: chapters
  type: entity
  entity: BinderItem
  relationship: ordered_one_to_many
  strong: true

# Shared references (N:M)
- name: tags
  type: entity
  entity: BinderTag
  relationship: many_to_many

Weak Relationships

Both many_to_one and many_to_many are always weak — they reference entities owned elsewhere. They cannot have strong: true. In Qleany, the owning side (with a strong relationship) controls cascade deletion. For many_to_one and many_to_many, theorically, it would mean that a child would be deleted only if there was no parent left to “own” it. Too difficult to implement, so no.

Dev note: theoretically, you can play with the junction table code base to support many_to_one with strong ownership, but that would be a nightmare to maintain and reason about.

entities:
  - name: Work
    fields:
      - name: tags                        # Owns the tags (strong one-to-many)
        type: entity
        entity: BinderTag
        relationship: one_to_many
        strong: true

  - name: Binder
    fields:
      - name: items                       # Owns the items (strong ordered)
        type: entity
        entity: BinderItem
        relationship: ordered_one_to_many
        strong: true

  - name: BinderItem
    fields:
      - name: binder                      # Back-reference (weak many-to-one)
        type: entity
        entity: Binder
        relationship: many_to_one

      - name: tags                        # Shared reference (weak many-to-many)
        type: entity
        entity: BinderTag
        relationship: many_to_many

Rule of thumb: Every entity referenced by a weak relationship (many_to_one or many_to_many) must be strongly owned somewhere else in your entity graph. Without strong ownership, entities become orphans with no lifecycle management.


Features and Use Cases

Features group related use cases together.

features:
  - name: file_management
    use_cases:
      - name: load_file
        # ...
      - name: save_file
        # ...

Use Case Options

OptionTypeDefaultDescription
namestringrequiredUse case name (snake_case)
undoableboolfalseGenerate undo/redo command scaffolding
read_onlyboolfalseNo data modification (affects generated code)
long_operationboolfalseAsync execution with progress (Rust only)
entitieslist[]Entities this use case works with
dto_inobjectnoneInput DTO for this use case
dto_outobjectnoneOutput DTO for this use case

In Rust, entities are doing a bit of the legwork for you to define which repositories are injected into the use case struct and prepare the use of a special macro macros::uow_action to simplify unit of work handling. These macro lines must be adapted in your use cases files, and the exact same macros must be repeated in these use cases’ unit of work files. Commentary lines will be generated to help you find and adapt these lines.

Similar macros are offered on the C++/Qt side.

DTOs

Each use case can have input and output DTOs, or only one, or none at all.

use_cases:
  - name: import_inventory
    dto_in:
      name: ImportInventoryDto
      fields:
        - name: file_path
          type: string
        - name: skip_header
          type: boolean
        - name: inventory_type
          type: enum
          enum_name: InventoryType
          enum_values:
            - Full
            - Incremental
    dto_out:
      name: ImportReturnDto
      fields:
        - name: imported_count
          type: integer
        - name: error_messages
          type: string
          is_list: true

You can’t put entities in DTOs. Only primitive types are allowed because entities are tied to the database and business logic, while DTOs are simple data carriers. Think of it as a way to control the data flow between the UI and the business logic. You don’t want to expose a password to the user UI just because it’s in an entity.

DTO Field Options

OptionTypeDefaultDescription
namestringrequiredField name (snake_case)
typestringrequiredField type (boolean, integer, float, string, uuid, datetime)
is_listboolfalseField is a list/array
optionalboolfalseField can be Option<>/std::optional
enum_namestringnoneFor enum type, name of the enum
enum_valueslistnoneFor enum type, list of possible values

User Interface Options


  ui:
    rust_cli: true
    rust_slint: true
    cpp_qt_qtwidgets: false
    cpp_qt_qtquick: false


These options allow the generation of scaffolding for the UI. They will each live in their own separate folders, also separate from the common backend code.

One backend, many frontends.