DcuplAppLoader API
The DcuplAppLoader handles fetching and processing configuration and data from dcupl Console or custom sources.
Installation
npm install @dcupl/loaderBasic usage
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
// projectId and apiKey belong to the dcupl instance, the loader reads them from there
const dcupl = new Dcupl({
config: { projectId: 'my-project', apiKey: 'my-api-key' },
});
const loader = new DcuplAppLoader();
// Register the loader first: config.fetch() and process() need it
dcupl.loaders.add(loader, 'main');
// Fetch the loader configuration (dcupl.lc.json) from the dcupl CDN
await loader.config.fetch();
// Download the models and data listed in the configuration
await loader.process();
// Build models, references and indices
await dcupl.init();The order matters: if the loader has not been added to a dcupl instance, config.fetch() throws and process() logs the error and resolves false. dcupl.init() does not process loaders. Call loader.process() before dcupl.init() (or before dcupl.update() when you load more data later).
process() without options loads every resource in the configuration. To load only the resources of one application, pass its key: see Applications. For a step-by-step setup against a console project (project ID, Browser key, version, environments), see Load from the console.
Setup errors
| Call | Throws |
|---|---|
config.fetch() before dcupl.loaders.add() |
dcupl not connected. Did you add your loader to dcupl? -> dcupl.loaders.add(loader) |
config.fetch() when the request fails or the response is not JSON |
Invalid Loader Configuration (the cause is logged first) |
process(), resources.plan(), resources.set(), add() or delete() without a configuration |
There is no config present. Make sure to call loader.config.set(...) or loader.config.fetch(...) first |
process({ applicationKey }) with an unknown key |
The applicationKey <key> does not exist |
config
Manage loader configuration.
Methods
| Method | Signature | Description |
|---|---|---|
get |
() => ProjectConfiguration | undefined |
Get current configuration |
set |
(config: ProjectConfiguration) => void |
Set configuration manually |
fetch |
(options?: FetchOptions) => Promise<ProjectConfiguration | undefined> |
Fetch configuration from remote |
get() returns a copy and set() stores a copy, so changing the object afterwards does not change the loader's configuration.
ProjectConfiguration
The shape of the loader configuration (dcupl.lc.json). Loader configuration explains tags, applications and environments.
type ProjectConfiguration = {
resources: Resource[]; // see Resource Types below
applications?: {
key: string;
name?: string;
description?: string;
resourceTags?: (string | string[])[];
variables?: Variable[];
}[];
environments?: {
key: string;
description?: string;
variables?: Variable[];
headers?: HttpConfig[]; // request headers for resource downloads
queryParams?: HttpConfig[]; // query parameters appended to resource urls
}[];
options?: ProcessOptions['options']; // see ProcessOptions below
};
type Variable = { key: string; value: string | undefined; description?: string };
type HttpConfig = {
key: string;
value: string; // may contain ${variables}
tags?: string[]; // only for resources sharing one of these tags; without tags: every resource (see below for headers)
condition?: { applyIfVariableHasValue?: string }; // only when this variable has a non-empty value
};An environment's variables, headers and query parameters apply only to a process() run that selects it with environmentKeys; an application preset does not select environments. Headers are only sent with resources that have tags: an untagged resource gets no environment headers, even from a header without tags.
FetchOptions
type FetchOptions = {
baseUrl?: string; // Where the config file lives (default: your project's draft version on the dcupl CDN)
loaderFileName?: string; // Config file name (default: 'dcupl.lc.json')
headers?: { key: string; value: string }[]; // Request headers, used when fetching from a custom baseUrl
skipApply?: boolean; // Return the config without applying it to the loader
};There are no projectId or apiKey fetch options. Pass them to new Dcupl({ config: { projectId, apiKey, version } }): dcupl.loaders.add() copies them into the loader's projectId, apiKey and version variables (version defaults to 'draft'), and the loader appends the API key to its requests to the dcupl CDN. A custom baseUrl also becomes the value of ${baseUrl} in resource URLs, unless an environment, process or global variable baseUrl overrides it (the loader then logs a warning).
Examples
// Fetch from the dcupl CDN (projectId and apiKey come from new Dcupl({ config }))
await loader.config.fetch();
// Fetch from custom URL
await loader.config.fetch({
baseUrl: 'https://my-cdn.example.com/config',
loaderFileName: 'dcupl.loader.json',
headers: [{ key: 'Authorization', value: 'Bearer my-token' }],
});
// Get current config
const config = loader.config.get();
console.log(config?.resources.length);
// Set config manually (models are loaded from 'model' resources too)
loader.config.set({
resources: [
{ type: 'model', url: '/models/product.dcupl.json' },
{ key: 'products', type: 'data', url: '/data/products.json', model: 'Product' },
],
});resources
Manage data resources (models, data files, transformers, operators, scripts).
Methods
| Method | Signature | Description |
|---|---|---|
get |
(key: string) => Resource | undefined |
Get resource by key |
getAll |
() => Resource[] |
Get all resources |
getWithTag |
(tag: string) => Resource[] |
Get resources with specific tag |
set |
(resources: Resource[]) => void |
Replace all resources |
add |
(resource: Resource) => void |
Add a resource |
delete |
(key: string) => void |
Delete a resource by key |
plan |
(options?: ProcessOptions) => ResourcePlan |
Resolve what a process(options) call would load, without loading it |
get and delete match the optional key of a resource. set, add and delete need a configuration (fetched or set) first.
Resource types
type Resource = ModelResource | DataResource | TransformerResource | CustomOperatorResource | CustomScriptResource;
// Fields shared by every resource type
type ResourceBase = {
key?: string; // Optional; used by resources.get()/delete() and as the plan id
url: string; // May contain ${variables}
type: 'model' | 'data' | 'transformer' | 'operator' | 'script';
tags?: string[]; // For filtering resources and matching transformers
};
type ModelResource = ResourceBase & { type: 'model' }; // the file holds a model definition, an array of them, or a view definition
type DataResource = ResourceBase & {
type: 'data';
model: string; // Target model
options?: {
updateType?: 'set' | 'update' | 'upsert' | 'remove';
keyProperty?: string | string[];
keyPropertySeparator?: string;
autoGenerateKey?: boolean;
autoGenerateProperties?: boolean | { deep?: boolean; sampleSize?: number };
csvParserOptions?: {
delimiter?: string;
quoteChar?: string;
escapeChar?: string;
commentChar?: string;
};
};
};
type TransformerResource = ResourceBase & {
type: 'transformer';
applyTo?: string[]; // Tags of the data resources it runs on
transformerType?: 'rawFileTransformer' | 'parsedFileTransformer'; // Default: 'parsedFileTransformer'
};
type CustomOperatorResource = ResourceBase & {
type: 'operator';
operator: string; // The operator name usable in queries
};
type CustomScriptResource = ResourceBase & {
type: 'script';
scriptKey: string; // The key the script is registered under (see /sdk/guides/scripts)
};Data resources default to updateType: 'set': the resource's rows replace all rows of its model. When several data resources feed one model, the last one wins, so give the others options: { updateType: 'upsert' } (or pass options: { defaultDataContainerUpdateType: 'upsert' } to process()).
Resource plan
plan() answers "what would process(options) load?" without fetching anything. It performs no
I/O and mutates no state, so it is safe to call repeatedly. Application preset, environments and
variables are resolved exactly as process() resolves them.
The plan is total: every entry in config.resources comes back either in scheduled or in
skipped with a reason — nothing is dropped silently. This is what lets you tell "loaded
everything" apart from "loaded everything this application was allowed to see".
type ResourcePlan = {
scheduled: ResourceItem[]; // what process() would load — tag-filtered, urls resolved
skipped: SkippedResource[]; // declared but not scheduled for this run
variables: Record<string, any>; // the variables the scheduled urls were resolved with
run: RunContext; // the applicationKey / resourceTags / environmentKeys used
};
type SkippedResource = {
resource: Resource;
reason: 'no-tags' | 'tag-mismatch';
id?: string;
};Scheduled items are Resource plus:
| Field | Description |
|---|---|
id |
Stable identity. An authored resource.key wins (k:<key>); otherwise a hash of the raw url plus type/model (h:<hash>). Derived from the raw url so config and runtime compute the same id — use it to join a runtime item back to its config entry instead of matching urls. Also available standalone as resolveResourceId / resolveResourceIds from @dcupl/common. |
rawUrl |
The url as authored, before ${variable} substitution (url holds the resolved value). |
unresolvedVariables |
Variables referenced by rawUrl that are not defined in any variable layer. Such a variable is substituted with an empty string, so a non-empty array here distinguishes a broken config from a missing file. A defined variable without a value (for example the built-in projectId when none is configured) also becomes an empty string but is not listed. |
The same information reaches progress callbacks: ProgressItem carries skippedItems and run
alongside the existing totalItems / processedItems / downloadedItems.
Examples
// Preview what a run would load, without loading it
const plan = loader.resources.plan({ applicationKey: 'catalog', resourceTags: ['critical'] });
console.log(plan.scheduled.map((r) => r.id));
plan.skipped.forEach((s) => console.log(`${s.resource.url} skipped: ${s.reason}`));
// Catch a misconfigured variable before it becomes a download failure
const broken = plan.scheduled.filter((r) => r.unresolvedVariables?.length);
// Get all resources
const resources = loader.resources.getAll();
// Get resources by tag
const criticalResources = loader.resources.getWithTag('critical');
// Add a new data resource
loader.resources.add({
key: 'products-update',
type: 'data',
url: '/api/products/latest',
model: 'Product',
options: { updateType: 'upsert' },
tags: ['live-data'],
});
// Delete a resource
loader.resources.delete('products-update');variables
Access configuration variables with environment support. Variables are referenced as ${name} in resource URLs, headers and query parameters. When the same key is defined in several places, the later source wins: defaults < environment < process options < global. config.fetch({ baseUrl, loaderFileName }) writes its options into the defaults, so an environment, process or global variable of the same name wins over them; when that happens with a different value, the loader logs a warning naming the variable, the fetch value and the winning layer (once per process() run).
The built-in default variables are projectId, apiKey and version ('draft'), filled from new Dcupl({ config }); cdnBaseUrl ('https://cdn.dcupl.com/files/projects'); baseUrl ('${cdnBaseUrl}/${projectId}/${version}', or the baseUrl passed to config.fetch()); modelUrl, dataUrl and transformerUrl (${baseUrl}/models, /data, /transformers); loaderFileName ('dcupl.lc.json'); cdnVersion; and consoleApiUrl.
Variables inside variable values are substituted one level deep. With the default CDN baseUrl (${cdnBaseUrl}/${projectId}/${version}), ${baseUrl} resolves fully, but ${modelUrl}, ${dataUrl} and ${transformerUrl} keep placeholders, and unresolvedVariables does not flag them:
${dataUrl}/products.csv -> ${cdnBaseUrl}/${projectId}/${version}/data/products.csv
${baseUrl}/data/products.csv -> https://cdn.dcupl.com/files/projects/<projectId>/draft/data/products.csvWrite resource URLs with ${baseUrl}/data/... instead. When baseUrl is a literal URL (passed to config.fetch({ baseUrl }) or set as a variable), ${modelUrl}, ${dataUrl} and ${transformerUrl} resolve fully as well.
Methods
| Method | Signature | Description |
|---|---|---|
get |
(key: string, env?: string) => Variable | undefined |
Get variable by key |
getAll |
(env?: string) => Variable[] |
Get all variables |
getAllAsObject |
(env?: string) => Record<string, any> |
Get variables as key-value object |
Nested variable APIs
// Built-in default variables (projectId, apiKey, version, baseUrl, ...)
loader.variables.default.get(key);
loader.variables.default.getAll();
// Environment variables (from config environments)
loader.variables.environment.get(env, key);
loader.variables.environment.getAll(env);
// Process variables (passed during processing)
loader.variables.process.get(key);
loader.variables.process.getAll();
// Global variables (set at runtime)
loader.variables.global.get(key);
loader.variables.global.getAll();
loader.variables.global.set(key, value);
loader.variables.global.delete(key);Examples
// Get API URL variable, resolved for the production environment
const apiUrl = loader.variables.get('API_URL', 'production');
console.log(apiUrl?.value);
// Get all variables for production environment
const prodVars = loader.variables.getAll('production');
// Get variables as object for easy access
const vars = loader.variables.getAllAsObject('production');
console.log(vars.API_URL);
// Set global variable at runtime
loader.variables.global.set('AUTH_TOKEN', 'Bearer xyz');
// Use in resource URLs with interpolation
// URL: "${API_URL}/products" becomes "https://api.example.com/products"transformers
Transformers reshape a data resource's payload before dcupl ingests it. Declare them as transformer resources in the configuration: the url points to a JavaScript file whose body returns a (resource, data) => data function. process() loads them before the data and runs each one on every data resource whose tags share a tag with its applyTo (a transformer with an empty applyTo runs only on untagged data resources).
loader.config.set({
resources: [
{ type: 'model', url: '${baseUrl}/models/product.dcupl.json' },
{
type: 'transformer',
url: '${baseUrl}/transformers/normalize-prices.js',
applyTo: ['products'], // runs on data resources tagged 'products'
transformerType: 'parsedFileTransformer', // default
},
{ type: 'data', url: '${baseUrl}/data/products.csv', model: 'Product', tags: ['products'] },
],
});return (resource, items) =>
items.map((item) => ({
...item,
price: Number(item.price) || 0,
currency: item.currency || 'USD',
}));A parsedFileTransformer receives the parsed items. A rawFileTransformer runs before parsing: it receives the CSV text of a CSV file, or the parsed body of a JSON file.
Methods
| Method | Signature | Description |
|---|---|---|
get |
(options: { appyTo: string[] }) => TransformerConfigObject[] |
Registered transformers whose applyTo shares a tag with the list |
getAll |
() => TransformerConfigObject[] |
All registered transformers |
set |
(transformer: TransformerConfigObject) => void |
Register a transformer (replaces one with the same key) |
remove |
(key: string) => void |
Remove a transformer by key |
The option of transformers.get() is spelled appyTo. { applyTo: [...] } matches nothing.
Transformer structure
type TransformerConfigObject = {
key: string; // A transformer loaded from a resource is registered under its resolved url
applyTo: string[]; // Tags of the data resources it runs on
type: 'rawFileTransformer' | 'parsedFileTransformer';
transformer: (resource: AppLoaderConfiguration.Resource, input: any) => any;
};process() only runs the transformers of the transformer resources in the current run. A transformer registered with transformers.set() is used when its key equals the resolved url of such a resource: the loader then uses your function instead of downloading the file. Under any other key it is stored but not applied.
Examples
// Supply the function for a declared transformer resource instead of downloading the file
loader.transformers.set({
key: 'https://cdn.example.com/transformers/normalize-prices.js', // the resource's resolved url
applyTo: ['products'],
type: 'parsedFileTransformer',
transformer: (resource, items) =>
items.map((item: Record<string, any>) => ({ ...item, currency: item.currency || 'USD' })),
});
// Get registered transformers for a tag (note the spelling: appyTo)
const productTransformers = loader.transformers.get({
appyTo: ['products'],
});
// Remove a transformer
loader.transformers.remove('https://cdn.example.com/transformers/normalize-prices.js');Errors
- A
parsedFileTransformerthat throws is recorded as aTransformerErrorloader error. The resource's data is still loaded, untransformed. - A
rawFileTransformerthat throws fails the whole data resource (InvalidResource). - With
new Dcupl({ scripting: { enabled: false } })none of your scripts run:transformer,operatorandscriptresources are skipped, each recorded as aScriptingDisabledloader error and reported as a failed item in the progress.
Loader errors are recorded whether or not quality checks are enabled:
const loaderErrors = dcupl.quality.errors.summary().filter((group) => group.type === 'loader');
// e.g. [{ key: 'Product::::TransformerError', model: 'Product', errorGroup: 'ResourceError', errorType: 'TransformerError', type: 'loader', totalCount: 1, ... }]Processing
Process the loaded configuration and apply to dcupl.
process()
await loader.process(options?: ProcessOptions); // Promise<boolean>Downloads the scheduled resources and hands models and data to dcupl. Call it after config.fetch() or config.set() (it throws without a configuration) and before dcupl.init(). To load more resources later, call process() again and then await dcupl.update(). On a sealed instance (performance.retainStagedData: false or dcupl.seal(), see seal()) process() rejects with DcuplSealedError before it fetches anything, so await it before dcupl.init().
Running process() again on the same loader rebuilds the models it loads from their current files (changed types apply, removed attributes disappear, a model generated for a data file follows the current columns; properties are not duplicated), drops the resources that left the loader configuration since the last run (their views are unregistered, their models and rows removed, the rows of a removed data file gone), and leaves resources still in the configuration that the run does not select as they are. So the documented pattern of loading more with other resourceTags keeps working. After await dcupl.update() every existing list serves the new data. Data resources a run loads replace their model's rows again unless they use updateType: 'upsert'. A resource without a key is identified by its URL, so editing a literal URL counts as removing that resource and adding a new one; URLs built from ${baseUrl} are not affected. Replacing the configuration (config.set() or config.fetch() with other resources) and processing again drops the resources the new configuration does not have.
One app loader per model key. When a loader's process() would load model files, view files or data for a key that another loader of the same instance already loads, it rejects with SharedModelKeyError (exported from @dcupl/loader, with modelKey, owner and loader) and changes nothing. Split models across loaders by key, not by file. dcupl.loaders.remove(key) frees the loader's keys: its models, views and rows go on the next update(), and models and views defined in code go back to their code definition.
ProcessOptions
type ProcessOptions = {
applicationKey?: string; // Select an application preset (its resourceTags and variables), see Applications
environmentKeys?: string[]; // Select environment configurations
resourceTags?: (string | string[])[]; // Filter resources by tags
variables?: { key: string; value: string | undefined; description?: string }[]; // Override variables
options?: {
defaultDataContainerUpdateType?: 'set' | 'update' | 'upsert' | 'remove';
csvParserOptions?: CsvParserOptions;
missingModelHandling?: {
// default: true — a data resource whose model has no definition gets a model with inferred properties
autoGenerateProperties?: boolean | { deep?: boolean; sampleSize?: number };
};
};
};resourceTags entries are combined with OR; an entry that is itself an array is an AND group. ['critical', ['eu', 'live']] loads resources tagged critical or tagged both eu and live. When you filter by tags, untagged resources are skipped.
The CSV parser reads csvParserOptions from the resource's options or from the configuration's top-level options. csvParserOptions passed to process() has no effect.
options passed to process() replace the configuration's top-level options for that run; they are not merged. Repeat any configured option you still need, for example defaultDataContainerUpdateType.
An options.missingModelHandling object replaces the default one as a whole, so missingModelHandling: {} turns missing-model handling off, the same as autoGenerateProperties: false: the rows of a data resource whose model isn't declared then load into no model, unless the resource's own options.autoGenerateProperties is true.
Examples
// Process with specific environment
await loader.process({
environmentKeys: ['production'],
});
// Process only tagged resources
await loader.process({
resourceTags: ['critical', 'models'],
});
// Process with variable overrides
await loader.process({
variables: [{ key: 'API_URL', value: 'https://staging-api.example.com' }],
});CSV files
A data resource is parsed as CSV when its resolved URL ends with .csv or the response's content-type contains text/csv. Any other response is parsed as JSON. The cells are converted against the target model:
- The delimiter is detected from the first line (
,,;, tab or|) unlesscsvParserOptions.delimiteris set. - Key columns are kept as written.
- In every other column,
nullbecomesnulland an empty cell becomesundefined. intandfloatuseparseInt/parseFloat, so12abcbecomes12.booleanacceptstrue,false,1and0in any case (an empty cell is no value, anything else is aWrongDataType).jsoncells are parsed withJSON.parse.Array<...>properties andmultiValuedreferences are split on the attribute'sseparator(default,), or parsed as JSON when the cell starts with[.- A column the model doesn't declare is dropped and reported once as an
UnknownColumnerror, unless the model hasautoGenerateProperties. - A cell that can't be converted is dropped and reported as
WrongDataType. Parsing never throws.
With autoGenerateProperties (also on a model the loader generates for a data resource without one), undeclared columns arrive as strings and are converted by the inferred type (TRUE / False columns infer as boolean, 1 / 0 columns stay int; see autoGenerateProperties).
Applications
An application is a named preset in the configuration's applications[]: a key, an optional name and description, the resourceTags it loads and the variables for its runs. process({ applicationKey }) loads the resources of that application; resources.plan({ applicationKey }) shows which ones they are.
{
"applications": [
{ "key": "catalog", "resourceTags": ["catalog"] },
{ "key": "default", "name": "Default" }
],
"resources": [
{ "type": "model", "url": "${baseUrl}/models/product.dcupl.json", "tags": ["catalog"] },
{ "type": "data", "url": "${baseUrl}/data/products.csv", "model": "Product", "tags": ["catalog"] },
{ "type": "model", "url": "${baseUrl}/models/order.dcupl.json", "tags": ["orders"] },
{ "type": "data", "url": "${baseUrl}/data/orders.csv", "model": "Order", "tags": ["orders"] }
]
}await loader.process({ applicationKey: 'catalog' }); // Product only
await loader.process({ applicationKey: 'default' }); // no resourceTags: every resource
await loader.process(); // no application: every resource- Without
applicationKey,process()loads every resource (narrowed only byresourceTagsyou pass). - An application without
resourceTagsloads every resource. With tags, untagged resources are skipped. resourceTagsorvariablespassed toprocess()replace the application's own.- An unknown key throws
The applicationKey <key> does not exist, also when the configuration has noapplicationsat all. - Projects from
dcupl initand the console's starter projects definedefault(without tags, so it loads every resource). A configuration you write yourself may have none, and thenprocess({ applicationKey: 'default' })throws. Pass a key only when your configuration defines that application.
An application does not select environments: pass environmentKeys for that. Loader configuration shows how tags and applications fit together in dcupl.lc.json.
Progress tracking
Subscribe to loader progress events.
on()
import type { AppLoaderProgress } from '@dcupl/loader';
const unsubscribe = loader.on((progress: AppLoaderProgress.ProgressItem) => {
console.log(`${progress.processed}/${progress.total} resources processed`);
});
// Later: unsubscribe
unsubscribe();ProgressItem
The callback receives a snapshot of the whole run, not a single event — every list below is the current state, re-sent on each update.
type ProgressItem = {
total: number; // resources scheduled for this run
processed: number;
remaining: number; // 0 once the run is finished
progress: number; // percentage, 0-100
context: string; // the type of the resource just handled: 'model', 'data', 'transformer', ...
currentItem: ResourceItem; // the resource this update is about (undefined in the first update)
totalItems: ResourceItem[]; // scheduled — tag-filtered, urls resolved
processedItems: ResourceItem[];
downloadedItems: ResourceItem[];
failedItems: ResourceItem[];
remainingItems: ResourceItem[];
skippedItems?: SkippedResource[]; // declared but never scheduled, with a reason
run?: RunContext; // applicationKey / resourceTags / environmentKeys
};The first update is sent when the run starts, before any resource is handled, so guard
currentItem with ?.. A resource's outcome lives on the item, not on the event: success is a
boolean and status is the HTTP status code. There is no completion event — a run is done when
remaining === 0.
loader.on((progress) => {
if (progress.remaining !== 0) return;
for (const failure of progress.failedItems) {
// status is only set when a response arrived (not on a network error or a bad url)
console.error(`${failure.key ?? failure.url} failed with ${failure.status ?? 'no response'}`);
}
});Complete example
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
// Runs in the browser (navigator.language below)
async function initializeDcupl() {
const dcupl = new Dcupl({
config: {
projectId: 'my-project',
apiKey: 'my-read-only-api-key', // a scoped read-only key: it ships to every visitor
},
});
const loader = new DcuplAppLoader();
// Register with dcupl before fetching
dcupl.loaders.add(loader, 'main');
// Track progress
loader.on((progress) => {
const current = progress.currentItem?.key ?? progress.currentItem?.url;
console.log(`Loading: ${current ?? 'starting'} (${progress.processed}/${progress.total})`);
});
// Fetch configuration
await loader.config.fetch();
// Set runtime variables, usable as ${USER_LOCALE} in resource urls
loader.variables.global.set('USER_LOCALE', navigator.language);
// Download models and data with the production environment
await loader.process({
environmentKeys: ['production'],
});
// Initialize
await dcupl.init();
return dcupl;
}See also
- Setup options - Choosing between SDK-only and Loader approaches
- Loader configuration - The
dcupl.lc.jsonfile: resources, tags, applications, environments - Dcupl API - Main dcupl instance API
- Data management - Working with data