Derived properties
Derived properties automatically populate values from referenced models, enabling denormalization and computed fields without manual data duplication. They're perfect for pulling data across relationships to improve query performance and simplify data access.
What are derived properties?
A derived property is a computed field that automatically extracts values from a referenced model and stores them locally. Think of it as automatic denormalization.
Benefits:
- Query local properties instead of deep queries
- Faster filtering and searching
- Simplified data access
- Automatic updates when references change
{
key: 'customerName',
type: 'string',
derive: {
localReference: 'customer', // Navigate through this reference
remoteProperty: 'name', // Get this property value
},
}How it works:
Order (key: 'o1')
├─ customer: 'c1' // Reference to Customer
└─ customerName: 'Alice' // Derived from customer.nameReference
derive key |
Meaning |
|---|---|
localReference |
A reference of this model (single- or multi-valued). If it doesn't exist: MissingReference error |
remoteProperty |
A property of the referenced model, not a dotted path. If it doesn't exist: MissingProperty error |
separator |
Joins the values over a multi-valued reference into a string (default ', '); ignored for single-valued |
| Situation | Derived value |
|---|---|
| Single-valued reference | The remote value as it is; the declared type doesn't convert it |
Multi-valued reference, string, int, float or date type |
The values joined with separator, as a string |
Multi-valued reference, Array<…> (or any other) type |
The list of values |
Falsy remote values (0, false, '') over a multi-valued reference |
Left out |
| Single-valued reference is empty, or its key has no item | The property is left out (a missing item is a RemoteReferenceKeyNotFound error) |
| Multi-valued reference is empty, or none of its keys has an item | '' for string, int, float and date types, [] for Array<…> types |
| Some keys of a multi-valued reference have no item | Those keys are skipped; the others are joined or listed |
A referenced item changes on dcupl.update() |
The derived value is recomputed |
index: true builds an inverse index for derived properties over single-valued references (see Indexing Derived Properties). To derive a reference instead of a value, use a derived reference (derive: { localReference, remoteReference }, see Reference types); the derive option itself is listed in the Models API.
Basic derived property
Single-valued reference derivation
Pull a property from a single referenced item:
import { Dcupl } from '@dcupl/core';
import type { ModelDefinition } from '@dcupl/common';
const dcupl = new Dcupl();
// Customer model
const customerModel: ModelDefinition = {
key: 'Customer',
properties: [
{ key: 'name', type: 'string' },
{ key: 'email', type: 'string' },
{ key: 'country', type: 'string' },
],
data: [
{ key: 'c1', name: 'Alice Johnson', email: 'alice@example.com', country: 'USA' },
{ key: 'c2', name: 'Bob Smith', email: 'bob@example.com', country: 'Canada' },
],
};
// Order model with derived property
const orderModel: ModelDefinition = {
key: 'Order',
properties: [
{ key: 'orderId', type: 'string' },
{ key: 'total', type: 'int' },
// Derived property - automatically populated from customer.name
{
key: 'customerName',
type: 'string',
derive: {
localReference: 'customer',
remoteProperty: 'name',
},
},
],
references: [{ key: 'customer', type: 'singleValued', model: 'Customer' }],
data: [
{ key: 'o1', orderId: 'order1', total: 150, customer: 'c1' },
{ key: 'o2', orderId: 'order2', total: 200, customer: 'c1' },
{ key: 'o3', orderId: 'order3', total: 100, customer: 'c2' },
],
};
dcupl.models.set(customerModel);
dcupl.models.set(orderModel);
await dcupl.init();
const orderList = dcupl.lists.create({ modelKey: 'Order' });
const orders = orderList.catalog.query.items();
console.log(orders[0].customerName); // 'Alice Johnson' (derived!)
// Filter on the derived property instead of a deep query on customer.name
orderList.catalog.query.addCondition({
attribute: 'customerName',
operator: 'eq',
value: 'Alice Johnson',
});The examples below reuse these imports and start from a fresh const dcupl = new Dcupl() each.
Multi-valued reference derivation
Derive properties from multi-valued references (arrays):
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'int' },
],
data: [
{ key: 'p1', name: 'Laptop', price: 999 },
{ key: 'p2', name: 'Mouse', price: 29 },
{ key: 'p3', name: 'Keyboard', price: 79 },
],
};
const orderModel: ModelDefinition = {
key: 'Order',
properties: [
{ key: 'orderId', type: 'string' },
// Derive product names from multi-valued reference
{
key: 'productNames',
type: 'string',
derive: {
localReference: 'products',
remoteProperty: 'name',
separator: ', ', // Join multiple values
},
},
],
references: [{ key: 'products', type: 'multiValued', model: 'Product' }],
data: [
{ key: 'o1', orderId: 'order1', products: ['p1', 'p2'] },
{ key: 'o2', orderId: 'order2', products: ['p3'] },
],
};
dcupl.models.set(productModel);
dcupl.models.set(orderModel);
await dcupl.init();
const orderList = dcupl.lists.create({ modelKey: 'Order' });
const orders = orderList.catalog.query.items();
console.log(orders[0].productNames); // 'Laptop, Mouse'
console.log(orders[1].productNames); // 'Keyboard'Over a multi-valued reference, a string, int, float or date property gets the values joined with separator (default ', '; separator: ' & ' gives 'Laptop & Mouse'); an Array<…> property (or any other type) gets the list of values. Falsy values (0, false, '') are left out. Over a single-valued reference, the remote value is copied as it is and separator is ignored.
Chained derivations
derive follows exactly one reference, and remoteProperty is a property of the referenced model (not a dotted path). To reach further, derive the value on the intermediate model first and derive that property:
const countryModel: ModelDefinition = {
key: 'Country',
properties: [
{ key: 'name', type: 'string' },
{ key: 'code', type: 'string' },
],
data: [
{ key: 'us', name: 'United States', code: 'US' },
{ key: 'ca', name: 'Canada', code: 'CA' },
],
};
const customerModel: ModelDefinition = {
key: 'Customer',
properties: [
{ key: 'name', type: 'string' },
// Derive country name
{
key: 'countryName',
type: 'string',
derive: {
localReference: 'country',
remoteProperty: 'name',
},
},
],
references: [{ key: 'country', type: 'singleValued', model: 'Country' }],
data: [
{ key: 'c1', name: 'Alice', country: 'us' },
{ key: 'c2', name: 'Bob', country: 'ca' },
],
};
const orderModel: ModelDefinition = {
key: 'Order',
properties: [
{ key: 'orderId', type: 'string' },
// Derive customer's country name (2-level derivation)
{
key: 'customerCountryName',
type: 'string',
derive: {
localReference: 'customer',
remoteProperty: 'countryName', // This is already derived!
},
},
],
references: [{ key: 'customer', type: 'singleValued', model: 'Customer' }],
data: [
{ key: 'o1', orderId: 'order1', customer: 'c1' },
{ key: 'o2', orderId: 'order2', customer: 'c2' },
],
};
dcupl.models.set(countryModel);
dcupl.models.set(customerModel);
dcupl.models.set(orderModel);
await dcupl.init();
const orderList = dcupl.lists.create({ modelKey: 'Order' });
const orders = orderList.catalog.query.items();
console.log(orders[0].customerCountryName); // 'United States'
console.log(orders[1].customerCountryName); // 'Canada'
// Fast filtering on derived property
orderList.catalog.query.addCondition({
attribute: 'customerCountryName',
operator: 'eq',
value: 'United States',
});Examples
Filter by a referenced name
Filter products by their category's name without a deep query:
const categoryModel: ModelDefinition = {
key: 'Category',
properties: [
{ key: 'name', type: 'string' },
{ key: 'slug', type: 'string' },
],
data: [
{ key: 'c1', name: 'Electronics', slug: 'electronics' },
{ key: 'c2', name: 'Clothing', slug: 'clothing' },
],
};
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'int' },
// Derive category name for easy filtering
{
key: 'categoryName',
type: 'string',
index: true, // eq/in on categoryName are index lookups
derive: {
localReference: 'category',
remoteProperty: 'name',
},
},
],
references: [{ key: 'category', type: 'singleValued', model: 'Category' }],
data: [
{ key: 'p1', name: 'Laptop', price: 999, category: 'c1' },
{ key: 'p2', name: 'Mouse', price: 29, category: 'c1' },
{ key: 'p3', name: 'T-Shirt', price: 19, category: 'c2' },
],
};
dcupl.models.set(categoryModel);
dcupl.models.set(productModel);
await dcupl.init();
const productList = dcupl.lists.create({ modelKey: 'Product' });
productList.catalog.query.addCondition({
attribute: 'categoryName',
operator: 'eq',
value: 'Electronics',
});
const electronics = productList.catalog.query.items();
// Returns: Laptop, MouseSearch a multi-valued derivation
const tagModel: ModelDefinition = {
key: 'Tag',
properties: [
{ key: 'name', type: 'string' },
{ key: 'color', type: 'string' },
],
data: [
{ key: 't1', name: 'JavaScript', color: '#f7df1e' },
{ key: 't2', name: 'TypeScript', color: '#3178c6' },
{ key: 't3', name: 'React', color: '#61dafb' },
],
};
const articleModel: ModelDefinition = {
key: 'Article',
properties: [
{ key: 'title', type: 'string' },
// Derive tag names (multi-valued)
{
key: 'tagNames',
type: 'string',
derive: {
localReference: 'tags',
remoteProperty: 'name',
separator: ', ',
},
},
],
references: [{ key: 'tags', type: 'multiValued', model: 'Tag' }],
data: [
{ key: 'art1', title: 'Getting Started with TS', tags: ['t1', 't2'] },
{ key: 'art2', title: 'React Hooks Guide', tags: ['t1', 't3'] },
],
};
dcupl.models.set(tagModel);
dcupl.models.set(articleModel);
await dcupl.init();
const articleList = dcupl.lists.create({ modelKey: 'Article' });
const articles = articleList.catalog.query.items();
console.log(articles[0].tagNames); // 'JavaScript, TypeScript'
console.log(articles[1].tagNames); // 'JavaScript, React'
// Search in derived tag names ('/TypeScript/' matches anywhere in the value)
articleList.catalog.query.addCondition({
attribute: 'tagNames',
operator: 'find',
value: '/TypeScript/',
});
// → Getting Started with TSDate derived properties
const projectModel: ModelDefinition = {
key: 'Project',
properties: [
{ key: 'name', type: 'string' },
{ key: 'deadline', type: 'date' },
],
data: [{ key: 'proj1', name: 'Website Redesign', deadline: '2025-12-31' }],
};
const taskModel: ModelDefinition = {
key: 'Task',
properties: [
{ key: 'title', type: 'string' },
// Derive project deadline
{
key: 'projectDeadline',
type: 'date',
derive: {
localReference: 'project',
remoteProperty: 'deadline',
},
},
],
references: [{ key: 'project', type: 'singleValued', model: 'Project' }],
data: [{ key: 't1', title: 'Design mockups', project: 'proj1' }],
};
dcupl.models.set(projectModel);
dcupl.models.set(taskModel);
await dcupl.init();
const taskList = dcupl.lists.create({ modelKey: 'Task' });
// Filter by project deadline (compare dates with a Date; an ISO string matches nothing)
taskList.catalog.query.addCondition({
attribute: 'projectDeadline',
operator: 'lte',
value: new Date('2025-12-31'),
});Array derived properties
const vendorModel: ModelDefinition = {
key: 'Vendor',
properties: [
{ key: 'name', type: 'string' },
{ key: 'certifications', type: 'Array<string>' },
],
data: [{ key: 'v1', name: 'TechCorp', certifications: ['ISO9001', 'ISO27001'] }],
};
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
// Derive array property (copied as it is over a single-valued reference)
{
key: 'vendorCertifications',
type: 'Array<string>',
derive: {
localReference: 'vendor',
remoteProperty: 'certifications',
},
},
],
references: [{ key: 'vendor', type: 'singleValued', model: 'Vendor' }],
data: [{ key: 'p1', name: 'Widget', vendor: 'v1' }],
};
dcupl.models.set(vendorModel);
dcupl.models.set(productModel);
await dcupl.init();
const productList = dcupl.lists.create({ modelKey: 'Product' });
const products = productList.catalog.query.items();
console.log(products[0].vendorCertifications); // ['ISO9001', 'ISO27001']Performance considerations
Query performance
Before (Deep Query):
orderList.catalog.query.addCondition({
attribute: 'customer.name',
operator: 'eq',
value: 'Alice',
});After (Derived Property):
orderList.catalog.query.addCondition({
attribute: 'customerName', // Derived property
operator: 'eq',
value: 'Alice',
});A deep query resolves the reference on every evaluated item; a derived property is stored on the item when the data is processed, and with index: true an eq condition becomes an index lookup. The gap grows with the number of items and the depth of the path. Measure with your own data (see Profiling).
Memory vs speed trade-off
A derived property stores one more value on every item and is recomputed when the referenced items change, so it costs memory and some processing time in init() and update(). Derive the values you query often, not every field of the referenced model.
When to use derived properties
Use derived properties when:
- You frequently query on referenced properties
- Query performance is critical
- The referenced data changes infrequently
- You need fast facets/aggregations
Avoid derived properties when:
- The referenced data changes constantly
- Memory is very constrained
- You rarely query the property
- Deep queries are fast enough for your use case
Indexing derived properties
Set index: true on a derived property over a single-valued reference to build its inverse index while the data is processed, so eq and in conditions on it are index lookups. Derived properties over multi-valued references are not indexed. (filter: true doesn't index; it only lists the property in catalog.filters for filter UIs.)
{
key: 'customerName',
type: 'string',
index: true,
derive: {
localReference: 'customer',
remoteProperty: 'name',
},
}Best practices
1. Index derived properties you filter by exact value
// ✅ Good: indexed, so eq/in conditions are index lookups
{
key: 'categoryName',
type: 'string',
index: true,
derive: {
localReference: 'category',
remoteProperty: 'name',
},
}
// ❌ Bad: no index, so every eq/in condition scans the items
{
key: 'categoryName',
type: 'string',
derive: {
localReference: 'category',
remoteProperty: 'name',
},
}2. Declare the remote property's type
The derived value is copied as it is (see Type Mismatch Errors), so give the derived property the type of the remote property: a float remote property gets a float derived property.
3. Use descriptive names
Prefix the derived property with the reference it comes from, such as customerName, categoryTitle or authorFullName, rather than name, title or value.
4. Derive only what you need
Each derived property adds a value to every item. Derive the fields you filter, sort, facet or display (for example customerName and customerEmail); read the rest with a projection when you need it.
Troubleshooting
Derived property is undefined
Problem: Derived property shows as undefined
console.log(order.customerName); // undefinedSolutions:
- Check reference exists. Over a single-valued reference, dcupl leaves the derived property out when the reference is empty or its key has no item (over a multi-valued reference you get
''or[]instead):
console.log(order.customer); // { key: 'c1' }, not undefined- Verify reference is resolved:
await dcupl.init(); // Must call init()- Check remote property exists. Items list a reference as
{ key }only, so project it to see the remote values:
const [order] = orderList.catalog.query.items({ projection: { $: true, customer: { $: true } } });
console.log(order.customer.name); // Verify source propertydcupl.quality.errors.summary() also lists the cause: RemoteReferenceKeyNotFound (the referenced key has no item), MissingProperty (remoteProperty isn't a property of the referenced model) or MissingReference (localReference isn't a reference of this model).
Type mismatch errors
Problem: The derived value doesn't have the type the property declares
Solution: dcupl copies the remote value as it is: the derived property's type doesn't convert it (over a multi-valued reference it only decides between a joined string and a list). Declare the remote property's type:
// Remote property
{ key: 'price', type: 'float' }
// Derived property (must match)
{ key: 'categoryPrice', type: 'float', derive: {...} }Multi-valued derivation not working
Problem: Multi-valued derivation returns incorrect results
Solution: Ensure reference is multi-valued:
references: [
{ key: 'tags', type: 'multiValued', model: 'Tag' }, // Must be multiValued
];
properties: [
{
key: 'tagNames',
type: 'string',
derive: {
localReference: 'tags',
remoteProperty: 'name',
separator: ', ',
},
},
];Performance issues
Problem: Derived properties not improving performance
Solution: Set index: true (derived properties over single-valued references) for properties you filter with eq or in:
{
key: 'customerName',
type: 'string',
index: true,
derive: {
localReference: 'customer',
remoteProperty: 'name',
},
}