Data flow
Understanding how data flows through dcupl helps you make better architectural decisions. This page traces data from external sources to query results.
Overview
Data in dcupl flows through four stages:
flowchart LR SOURCE["1. Source<br/>CSV, JSON, API"] LOAD["2. Load<br/>Parse & Transform"] STORE["3. Store<br/>In-Memory"] QUERY["4. Query<br/>Filter & Retrieve"] SOURCE --> LOAD LOAD --> STORE STORE --> QUERY
Each stage has specific responsibilities and components.
Stage 1: data sources
Data originates from external sources. dcupl supports various formats:
| Format | Description | Use Case |
|---|---|---|
| JSON | Native format, no parsing needed | APIs, exports |
| CSV | Comma-separated values | Spreadsheets, databases |
| API | REST endpoints | Live data |
Data sources can be:
- Static files - Hosted on CDN or bundled with your app
- Dynamic endpoints - APIs that return fresh data
- Workflow outputs - Transformed data from Console workflows
Stage 2: loading
The loading stage fetches and parses raw data and stages it for the model. Processing against the model definitions happens afterwards, in dcupl.init() or dcupl.update().
flowchart TD
subgraph loading["Loading Stage"]
FETCH["Fetch Data"]
PARSE["Parse Format"]
TRANSFORM["Apply Loader Transformers"]
STAGE["Stage Data Container"]
end
SOURCE[Data Source] --> FETCH
FETCH --> PARSE
PARSE --> TRANSFORM
TRANSFORM --> STAGE
STAGE --> PROCESS["dcupl.init() / update()<br/>Process & Validate Against Model"]
PROCESS --> STORE[In-Memory Store]
Loader transformers are JavaScript resources: a raw-file transformer runs on the fetched file before parsing, a parsed-file transformer on the parsed items.
DcuplAppLoader
The DcuplAppLoader handles loading from a LoaderConfiguration:
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
const dcupl = new Dcupl({
config: { projectId: 'your-project', apiKey: 'your-api-key' },
});
const loader = new DcuplAppLoader();
dcupl.loaders.add(loader);
// 1. Fetch configuration
await loader.config.fetch();
// 2. Load the resources it lists
await loader.process();
// 3. Apply the loaded models and data
await dcupl.init();The loader reads dcupl.lc.json which defines:
{
"resources": [
{
"url": "${baseUrl}/models/product.dcupl.json",
"type": "model",
"tags": ["product"]
},
{
"url": "${baseUrl}/data/products.json",
"type": "data",
"model": "Product",
"tags": ["product"]
}
]
}loader.process() loads every resource. To load only some of them, pass resourceTags, or define applications (named sets of resources) as described in Loader.
Manual loading
You can also load data directly without a LoaderConfiguration:
const dcupl = new Dcupl();
// Add model
dcupl.models.set({
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'int' },
],
});
// Add data
dcupl.data.set(
[
{ key: 'p1', name: 'Laptop', price: 999 },
{ key: 'p2', name: 'Mouse', price: 29 },
],
{ model: 'Product' },
);
// Data calls are staged: init() (later: update()) applies them
await dcupl.init();Data transformations
When init() (or update()) processes the staged data, dcupl applies transformations defined in models:
Type Coercion - Converts strings to proper types
// Raw CSV data: "29.99"
// Model property: { key: 'price', type: 'float' }
// Result: 29.99 (number)Array Conversion - Splits delimited strings into arrays
// Raw data: "red,blue,green"
// Property: { key: 'colors', type: 'Array<string>', separator: ',' }
// Result: ["red", "blue", "green"]Value Mapping - Translates codes to readable values
// Raw data: "1"
// Model: valueMappings: [{ attributes: ['status'], values: [{ from: ['1'], to: 'active' }] }]
// Result: "active"Stage 3: in-memory storage
After loading, data lives in memory organized by model.
dcupl.init() processes every model after the models it references. It handles plain properties and references before the attributes computed from them (resolved, query, derived and grouped references, derived and expression properties), so a derived property or derived reference that reads a resolved reference still gets its value. dcupl.update() re-processes only the affected attributes when the change is small, no model was replaced with data.set(), no model definition changed since init() and no query reference is affected (a partial update), and runs the full pipeline otherwise.
Each model holds its items by their key (or the model's keyProperty). That key is how query.one({ itemKey }), data.update() and data.remove() find an item. When rows loaded together share a key, the last one wins (see Data Key Strategy).
Indices
dcupl indexes attributes to speed up queries. References and properties marked index: true are indexed while the data is processed; other properties are indexed on demand (for example when you request facets for them).
eq and in filters use an existing index instead of scanning all items; other operators scan the items.
Reference graph
dcupl processes a model after the models it references. A reference cycle between models (A → B → A) makes init() fail: it logs the cycle and resolves to false. A model may reference itself (for example a parent reference); that is not a cycle.
graph LR Product -->|"category"| Category Order -->|"customer"| Customer Order -->|"items"| Product
References link items across models, which enables deep queries such as category.name.
Stage 4: querying
Queries retrieve and filter data from the in-memory store.
flowchart TD
subgraph query["Query Execution"]
CREATE["Create List"]
BUILD["Build Query"]
EXECUTE["Execute Filter"]
RESULT["Return Results"]
end
STORE[In-Memory Store] --> CREATE
CREATE --> BUILD
BUILD --> EXECUTE
EXECUTE --> RESULT
Lists
A List is a filtered view of a model's data:
const productList = dcupl.lists.create({
modelKey: 'Product',
});Multiple lists can exist for the same model, each with different filters.
Query building
Build queries using the Catalog API:
// Add filter conditions
productList.catalog.query.addCondition({
attribute: 'price',
operator: 'gte',
value: 100,
});
productList.catalog.query.addCondition({
attribute: 'category',
operator: 'eq',
value: 'Electronics',
});Query execution
A list filters the items with its conditions, then sorts them, then cuts out the page (start, count):
flowchart LR QUERY["Query Conditions"] FILTER["Filter Items"] SORT["Apply Sorting"] PAGE["Apply Pagination"] ITEMS["Return Items"] QUERY --> FILTER FILTER --> SORT SORT --> PAGE PAGE --> ITEMS
// Execute and get results
const items = productList.catalog.query.items();Each list caches its query results and reuses them until an update() changes the list's data. The cache is bounded by performance.cache.maxSize (default 64 MiB; the least recently used entries are dropped first), see Caching.
Additional operations
Beyond filtering, the Catalog provides:
Facets - Count items by property values
const categoryFacets = productList.catalog.fn.facets({ attribute: 'category' });
const brandFacets = productList.catalog.fn.facets({ attribute: 'brand' });
// categoryFacets: [{ value: 'Electronics', size: 4, selected: true, ... }, { value: 'Clothing', size: 1, selected: false, ... }]
// brandFacets: [{ value: 'BrandA', size: 3, ... }, { value: 'BrandB', size: 1, ... }]A facet ignores the list's own condition on the faceted attribute, so Clothing keeps its count while the list shows only Electronics. Conditions on other attributes (here price) do reduce the counts.
Aggregations - Calculate statistics
const stats = productList.catalog.fn.aggregate({
attribute: 'price',
types: ['min', 'max', 'avg', 'sum', 'count'],
});
// { attribute: 'price', types: [...], min: 149, max: 999, avg: 511.5, sum: 2046, count: 4 }Sorting and Pagination - Order and limit results
// Page 1, 20 items per page, cheapest first
productList.catalog.query.applyOptions({
sort: { attributes: ['price'], order: ['ASC'] },
start: 0,
count: 20,
});Complete flow example
Here is the full data flow from source to query result:
sequenceDiagram participant Source as Data Source participant Loader as DcuplAppLoader participant Store as In-Memory Store participant List as DcuplList participant App as Your Application Source->>Loader: JSON/CSV data Loader->>Loader: Parse & transform Loader->>Store: Stage data Store->>Store: init() processes data, builds indices App->>List: Create list App->>List: Add conditions List->>Store: Execute query Store->>List: Filtered items List->>App: Results
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
// 1. Initialize
const dcupl = new Dcupl({ config: { projectId: 'demo', apiKey: 'demo-api-key' } });
const loader = new DcuplAppLoader();
dcupl.loaders.add(loader);
// 2. Load (Source -> Parse -> Store)
await loader.config.fetch();
await loader.process();
await dcupl.init();
// 3. Query
const list = dcupl.lists.create({ modelKey: 'Product' });
list.catalog.query.addCondition({
attribute: 'category',
operator: 'eq',
value: 'Electronics',
});
// 4. Results
const items = list.catalog.query.items();
const brandFacets = list.catalog.fn.facets({ attribute: 'brand' });Performance considerations
Loading
- Load only needed data using resource tags
- Use CDN for faster delivery
- Consider data size: what fits depends on your data and the device (see Data size and limits); data too large for the browser belongs on a server-hosted REST API (RAPI) instance
Storage
- References and
index: trueproperties are indexed at load time, other properties on demand (e.g. for facets) - Memory usage scales with data size
- Reference resolution adds overhead for deeply nested queries
Querying
eqandinconditions on references andindex: trueproperties are fast index lookups- Complex queries with many conditions may scan more items
- Facet calculations touch all matching items