# Models

Define the structure and schema of your data with properties, types, and references.

## What is a model?

A `ModelDefinition` is a configuration object that describes the structure and schema of your data.

- **Properties**: the fields in your data structure
- **Data types**: the type of each property (string, int, boolean…)
- **References**: relationships to other models
- **Data**: optional inline data

Think of models as **blueprints** or **table schemas** for your data.

```typescript filename="product.model.ts"
import type { ModelDefinition } from '@dcupl/common';

const productModel: ModelDefinition = {
  key: 'Product',
  properties: [
    { key: 'name', type: 'string' },
    { key: 'sku', type: 'string', index: true },
    { key: 'price', type: 'float' },
    { key: 'tags', type: 'Array<string>' },
    { key: 'inStock', type: 'boolean' },
  ],
  references: [
    { key: 'category', type: 'singleValued', model: 'Category' },
  ],
};
```

*Product — 5 properties · 1 reference · what this model holds once data is loaded — Demo data*

| sku , string | name , string | price , float | tags , Array<string> | inStock , boolean | category , → Category |
| --- | --- | --- | --- | --- | --- |
| SKU-1001 | NYM-J cable 3x1.5, 50 m | 42.9 | indoor, 3-core | true | cable |
| SKU-2001 | Rocker switch, white | 6.9 | flush | true | switch |
| SKU-2002 | LED dimmer switch, white | 48 | led, flush | false | switch |
| SKU-3001 | LED panel 620×620, 36 W | 89.5 | led, ceiling | true | lighting |

Point at a property in the code to find its column. category holds the key of a Category item.

## Property types

dcupl supports these data types:

| Type | Description | Example |
| --- | --- | --- |
| `string` | Text values | `"Laptop"` |
| `int` | Whole numbers | `42` |
| `float` | Decimal numbers | `99.99` |
| `boolean` | True/false | `true` |
| `date` | Date/time values | `"2025-01-15"` |
| `json` | Complex nested objects | `{ color: 'red' }` |
| `any` | Any value, stored as is | `42`, `'a'` |
| `Array<string>`, `Array<int>`, `Array<float>`, `Array<date>` | Typed arrays | `['red', 'blue']` |

A `boolean` property also reads the strings `'true'`/`'false'`/`'1'`/`'0'` (any case) and the numbers `1`/`0`. Any other string or number, and any object or array, is a `WrongDataType` quality error, and the property stays `undefined`. With `quality.forceStrictDataType`, only `true`/`false` pass.

There is no plain `array` type. A property with any other `type` is rejected with an `InvalidModelDefinition` quality error ("Property type is invalid"), and its values are not loaded.

```typescript filename="property-types.ts"
const productModel: ModelDefinition = {
  key: 'Product',
  properties: [
    { key: 'name', type: 'string' },
    { key: 'price', type: 'float' },
    { key: 'rating', type: 'float' },
    { key: 'inStock', type: 'boolean' },
    { key: 'createdAt', type: 'date' },
    { key: 'metadata', type: 'json' },
    { key: 'tags', type: 'Array<string>' },
  ],
};
```

## Property options

Properties support additional options for indexing, computed values, and validation.

#### index

Queries and facets work without it: dcupl builds a property's lookup index on the first query that needs it. `index: true` builds it at load time instead, so use it for attributes you filter on often.

#### derive

Create computed properties from references. Pull values from referenced models automatically.

#### expression

Template-based computed properties using `${property}` placeholders. The result is always a string.

#### quality

Data validation rules: required fields and built-in validators (`email`, `pattern`, `min`/`max`, `enum`, `unique`, …).

More options (`filter`, `aggregate`, `origin`, `separator`, `inputFormat` and `UTC` for dates, `fractionDigits`) are in the [Models API reference](/sdk/api/models#property).

```typescript filename="property-options.ts"
const orderModel: ModelDefinition = {
  key: 'Order',
  properties: [
    // Index built at load time instead of on first query
    { key: 'orderId', type: 'string', index: true },

    // Derived from reference
    {
      key: 'customerName',
      type: 'string',
      derive: {
        localReference: 'customer',
        remoteProperty: 'name',
      },
    },

    // Template expression
    {
      key: 'displayText',
      type: 'string',
      expression: 'Order #${orderId} - ${customerName}',
    },

    // Validation rules
    {
      key: 'email',
      type: 'string',
      quality: {
        required: true,
        validators: { email: {} },
      },
    },
  ],
  references: [
    { key: 'customer', type: 'singleValued', model: 'Customer' },
  ],
};
```

## References

References link one model to another, enabling:

- Navigate between related data
- Query across relationships (deep queries)
- Build complex data structures
- Maintain data normalization

#### Single-valued (many-to-one)

Each item references **one** other item. Use for: Order → Customer, Product → Category.

#### Multi-valued (to many)

Each item holds a **list** of keys; the same target can be referenced by many items. Use for: Order → Products, User → Roles.

References can also be computed (`resolve`, `derive`, `groupBy`, `query`) or check their keys (`validity`): see [Reference](/sdk/api/models#reference) in the Models API.

```typescript filename="references.ts"
const orderModel: ModelDefinition = {
  key: 'Order',
  properties: [
    { key: 'total', type: 'int' },
  ],
  references: [
    // Single-valued: one customer per order
    {
      key: 'customer',
      type: 'singleValued',
      model: 'Customer',
    },
    // Multi-valued: many products per order
    {
      key: 'products',
      type: 'multiValued',
      model: 'Product',
    },
  ],
};

// Data format:
// { key: 'o1', total: 150, customer: 'c1', products: ['p1', 'p2'] }
```

## Resolved references

Create the **inverse** of another reference automatically using `resolve`.

Instead of manually maintaining both directions of a relationship, dcupl can compute the inverse for you.

Example: If Order → Customer exists, you can automatically get Customer → Orders.

The resolved reference is computed at initialization time and stays in sync with your data.

```typescript filename="resolved-refs.ts"
// Customer model with inverse reference
const customerModel: ModelDefinition = {
  key: 'Customer',
  properties: [
    { key: 'name', type: 'string' },
    { key: 'email', type: 'string' },
  ],
  references: [
    {
      key: 'orders',
      type: 'multiValued',
      model: 'Order',
      // Automatically resolve from Order.customer
      resolve: { reference: 'customer', model: 'Order' },
    },
  ],
};

// Now customer.orders contains all orders
// for that customer automatically
```

## Model options

Models support additional configuration options:

| Option | Description |
| --- | --- |
| `keyProperty` | Use a custom property as item identifier (default: `'key'`). An array builds a composite key: `['ID', 'Language']` gives items keys like `'A1::en'` |
| `keyPropertySeparator` | Separator joining the parts of a composite key (default: `'::'`) |
| `autoGenerateKey` | Generate unique keys for items without one |
| `autoGenerateProperties` | Infer properties from the data: `true` reads the first row and checks number columns against every row, `{ deep: true }` all rows, `{ sampleSize: n }` up to n rows. String values like `"42"` or `"true"` are typed as numbers, booleans, or dates; zero-padded values like `"007"` stay strings. |
| `supportsAutoCreation` | Auto-create an item of this model when another model references a key it doesn't have |
| `valueMappings` | Transform values during data loading |
| `meta` | Attach metadata to the model |
| `quality` | Configure validation rules |

```typescript filename="model-options.ts"
const productModel: ModelDefinition = {
  key: 'Product',

  // Use 'sku' as identifier instead of 'key'
  keyProperty: 'sku',

  // Require explicit keys
  autoGenerateKey: false,

  // Don't auto-create a Product when another model
  // references a Product key that doesn't exist
  supportsAutoCreation: false,

  // Attach metadata
  meta: {
    name: 'Products',
    icon: 'package',
  },

  properties: [
    { key: 'sku', type: 'string' },
    { key: 'name', type: 'string' },
    { key: 'price', type: 'float' },
  ],
};
```

## Quality

Define **quality criteria** to validate your data at both model and attribute level.

#### Model-level quality

Set defaults for the quality flags of all attributes of a model in `quality.attributes`. Validators can only be set per attribute. `quality.enabled: false` turns the checks off for the model.

#### Attribute-level quality

Override or extend quality rules for specific properties:

- `required` - Property must have a value. Defaults to `true`, so a missing value is reported unless you set `required: false`
- `nullable` - Allow `null` (default `false`)
- `forceStrictDataType` - Report and drop wrong-typed values instead of converting them (default `false`)
- `validators` - Validation rules: `email`, `pattern`, `startsWith`, `endsWith`, `includes`, `enum`, `min`/`max`, `minLength`/`maxLength`, `unique`. They run on `string`, `int` and `float` properties
- `validatorHandling` - `'loose'` (default) reports a failed validator and keeps the value, `'strict'` also drops it

Quality issues are reported during data loading. Read the totals with `dcupl.quality.errors.summary()` and the individual errors by querying the `$DcuplErrorTrackingErrors` model.

```typescript filename="quality.ts"
const userModel: ModelDefinition = {
  key: 'User',

  // Model-level quality: default rules for all attributes
  quality: {
    attributes: { required: false },
  },

  properties: [
    // Override: make this property required
    {
      key: 'email',
      type: 'string',
      quality: {
        required: true,
        validators: { email: {} },
      },
    },

    // Regex validation
    {
      key: 'phone',
      type: 'string',
      quality: {
        validators: {
          pattern: { value: '^\\+?[0-9]{10,14}$' },
        },
      },
    },

    // Prefix validation
    {
      key: 'website',
      type: 'string',
      quality: {
        validators: { startsWith: { value: 'https://' } },
      },
    },

    // No quality override - uses model defaults
    { key: 'nickname', type: 'string' },
  ],
};
```

## Meta

Attach **custom metadata** to models and attributes for documentation, tooling, or frontend logic.

#### Model meta

Describe the model itself:

- Display names and descriptions
- Data source information
- Icons or UI hints

#### Attribute meta

Describe individual properties:

- Labels for forms or tables
- Tooltips and help text
- Formatting hints (currency, percentage)
- Source system field mappings

Meta is a free-form object with a required `name` and an optional `description` - structure the rest however fits your needs.

```typescript filename="meta.ts"
const productModel: ModelDefinition = {
  key: 'Product',

  // Model-level metadata
  meta: {
    name: 'Products',
    description: 'Product catalog from ERP system',
    source: {
      system: 'SAP',
      table: 'MARA',
      refreshInterval: '15min',
    },
    ui: {
      icon: 'package',
      color: '#3b82f6',
    },
  },

  properties: [
    {
      key: 'name',
      type: 'string',
      // Attribute-level metadata
      meta: {
        name: 'Product Name',
        description: 'Official product name from catalog',
        searchable: true,
      },
    },
    {
      key: 'price',
      type: 'float',
      meta: {
        name: 'Price',
        format: 'currency',
        currency: 'EUR',
        source: { field: 'NETPR' },
      },
    },
    {
      key: 'rating',
      type: 'float',
      meta: {
        name: 'Customer Rating',
        format: 'stars',
        min: 0,
        max: 5,
      },
    },
  ],
};
```

## Best practices

- **Use clear, descriptive keys** - `Product`, `CustomerOrder` instead of `Item`, `Data`
- **Keep models focused** - Each model should represent a single entity
- **Use specific types** - `float` for prices, `int` for counts, `boolean` for flags, not `string`
- **Use typed arrays** - `Array<string>`, `Array<int>`, … (there is no untyped `array` type)
- **Index hot filter attributes** - `index: true` moves the index build from the first query to load time; every index costs memory
- **Choose the right reference type** - Single-valued for one key per item, multi-valued for a list of keys
- **Limit reference depth** - Deep queries (3+ levels) add overhead

```typescript filename="best-practices.ts"
// Good: Clear types, an index for a hot filter attribute
const productModel: ModelDefinition = {
  key: 'Product',
  properties: [
    { key: 'sku', type: 'string', index: true },
    { key: 'name', type: 'string' },
    { key: 'price', type: 'float' },
    { key: 'rating', type: 'float' },
    { key: 'inStock', type: 'boolean' },
    { key: 'tags', type: 'Array<string>' },
  ],
  references: [
    { key: 'category', type: 'singleValued', model: 'Category' },
  ],
};

// Avoid: String types for everything
// { key: 'price', type: 'string' }  // Bad
// { key: 'count', type: 'string' }  // Bad
```

## Next steps

- [Models API](/sdk/api/models) — Full reference: every model, property and reference option
- [Lists](/sdk/core-concepts/lists) — Create filtered views of your models
- [Query overview](/sdk/core-concepts/query-overview) — Learn the query interface
- [Derived properties](/sdk/advanced/derived-properties) — Compute values from references
