Lists API
The Lists API (dcupl.lists) provides methods for creating and managing isolated data collections with their own filters, queries, and state management.
Overview
Lists are isolated views of your data that maintain their own query state and provide access to the catalog API for querying, filtering, and analytics. Each list operates independently and can have different filters applied to the same underlying data model.
Methods
create()
Creates a new list instance.
Signature:
create<T extends RawItem = RawItem>(options: {
modelKey: string;
listKey?: string;
listItemKeys?: string[];
query?: DcuplGlobalQueryOptions<ListItem>;
}): DcuplList<T>Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
options |
DcuplListOptions |
Yes | List configuration options |
Returns: DcuplList<T> - The created list instance
create<Product>() returns a DcuplList<Product> (SDK 2.0): items(), first(), execute(), any() and count() keep that item type whatever options you pass (items({ sort }) no longer narrows it to the sorted fields). A type argument on a read must extend the list's item type: items<ProductWithStock>() adds fields, it cannot drop them (items<Pick<Product, 'key' | 'name'>>() is a type error). For a projected shape, create the list without a type argument (or with create<Partial<Product>>()) and type the read, items<{ key: string; name: string }>({ projection: { name: true } }), or cast the result. one() and many() take no type argument and return the list's item type. The published list query types use NoInfer, so consumers need TypeScript 5.4 or later.
Throws: Model with key <modelKey> not found when the model does not exist, which includes calling
create() before dcupl.init() has run. An existing listKey is not reused: create() adds a second
list with the same key.
A list created with listItemKeys or query is scoped: it keeps a set of keys (the
listItemKeys you pass, or the keys the query matches when the list is created), and its own
catalog query starts empty (clear() does not remove the scope). It is not an initial filter. The
list always serves the model's current items for those keys: after dcupl.update() changed values
show, removed items drop out, and an item added under one of the keys is served. For a
listItemKeys list that is all there is, so auto-update changes only
the events, not the items. Items that start matching a seed query join only when you call update()
or enable auto-update.
Examples:
const productList = dcupl.lists.create({
modelKey: 'Product',
});const activeProducts = dcupl.lists.create({
modelKey: 'Product',
listKey: 'activeProducts',
});const selectedProducts = dcupl.lists.create({
modelKey: 'Product',
listItemKeys: ['p1', 'p2', 'p3'],
});const expensiveProducts = dcupl.lists.create({
modelKey: 'Product',
query: {
modelKey: 'Product',
queries: [{ operator: 'gte', attribute: 'price', value: 100 }],
},
});import type { DcuplListOptions } from '@dcupl/common';
// autoUpdate is missing from the create() parameter type,
// so pass the options as a DcuplListOptions variable
const options: DcuplListOptions = {
modelKey: 'Product',
query: {
modelKey: 'Product',
queries: [{ operator: 'gte', attribute: 'price', value: 100 }],
},
autoUpdate: true,
};
const list = dcupl.lists.create(options);get()
Retrieves a list by its key.
Signature:
get(listKey: string): DcuplList | undefinedParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
listKey |
string |
Yes | The key of the list to retrieve |
Returns: DcuplList | undefined - The list instance or undefined if not found
Examples:
const list = dcupl.lists.get('activeProducts');
if (list) {
const items = list.catalog.query.items();
}const exists = dcupl.lists.get('myList') !== undefined;const list = dcupl.lists.get('myList');
if (list) {
// Use list
} else {
// Create list
const newList = dcupl.lists.create({
modelKey: 'Product',
listKey: 'myList',
});
}getAll()
Retrieves all lists or filters by model.
Signature:
getAll(filter?: { modelKey: string }): DcuplList[]Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
filter |
{ modelKey: string } |
No | Optional filter by model key |
Returns: DcuplList[] - Array of list instances
The result also contains the internal __global_list__<modelKey> lists that dcupl.fn.*,
dcupl.query.execute() and create() with a query set up. Skip keys starting with
__global_list__ when you only want your own lists.
Examples:
const allLists = dcupl.lists.getAll();
console.log(allLists.length);const productLists = dcupl.lists.getAll({ modelKey: 'Product' });dcupl.lists.getAll().forEach((list) => {
list.update();
});const listsByModel: Record<string, number> = {};
dcupl.lists.getAll().forEach((list) => {
listsByModel[list.modelKey] = (listsByModel[list.modelKey] || 0) + 1;
});destroy()
Destroys lists and cleans up resources.
Signature:
destroy(options?: { listKey?: string; modelKey?: string }): voidParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
options |
object |
No | Destroy options (all lists if omitted) |
options.listKey |
string |
No | Specific list key to destroy |
options.modelKey |
string |
No | Destroy all lists for a model |
Examples:
dcupl.lists.destroy({ listKey: 'activeProducts' });dcupl.lists.destroy({ modelKey: 'Product' });dcupl.lists.destroy();const list = dcupl.lists.get('myList');
if (list) {
dcupl.lists.destroy({ listKey: 'myList' });
}DcuplList class
The DcuplList class represents an individual list instance with its own state and catalog API.
Properties
key
The unique identifier for this list.
Type: string
Example:
const list = dcupl.lists.create({
modelKey: 'Product',
listKey: 'myList',
});
console.log(list.key); // 'myList'modelKey
The model this list is based on.
Type: string
Example:
console.log(list.modelKey); // 'Product'catalog
Access to the catalog API for querying and analytics.
Type: DcuplList<T>['catalog'], an instance of the Catalog class typed with the list's item type. Catalog is not exported from the @dcupl/core main entry; import it from @dcupl/core/catalog if you need the class itself.
Example:
const items = list.catalog.query.items();
const facets = list.catalog.fn.facets({ attribute: 'category' });Methods
update()
Re-derives the list from the current data: a query-scoped list re-runs its seed query (so new
matches join and items that stop matching leave), and the list's caches are rebuilt. A
listItemKeys list re-applies its keys, which gives the items it already serves after
dcupl.update(). Emits list_updated (not result_updated).
Signature:
update(options?: { reprocessData?: boolean }): voidParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
options |
object |
No | Update options |
options.reprocessData |
boolean |
No | update() and update({ reprocessData: true }) re-derive the list; an options object without it skips that (list_updated is still emitted) |
Examples:
list.update();dcupl.data.update([{ key: 'p1', price: 150 }], { model: 'Product' });
await dcupl.update();
list.update(); // Only needed to re-run the seed query of a query-scoped listawait dcupl.update() already drops the query caches of every affected list, so
an unscoped list returns the new data without any extra call, and so does a listItemKeys
list. Use update() when a list was created with a query scope and you want that query
re-run on the current data.
A scoped list that does not auto-update keeps its key set but serves current values:
after a partial update as well as after a full rebuild (data.set(), data.reset(), or an update
above the partial-update thresholds) its queries and
functions run on the model's current items, and removed items are gone. Only new items that would
match the seed query need list.update() or autoUpdate: true to join; a listItemKeys list
also serves an item added later under one of its keys.
const list = dcupl.lists.create(options); // a query-scoped list with autoUpdate: true (see create())
dcupl.data.upsert([{ key: 'p9', name: 'New Product', price: 120 }], { model: 'Product' });
await dcupl.update(); // re-derives the list: p9 joins it without a list.update() callinvalidate()
Drops the list's query and result caches. The filtered data is recomputed lazily
on the next read, from the list's current data — without re-deriving a scoped
list's item set (that is what update() does).
dcupl.update() calls this automatically for every affected list that does not
auto-update, so a query executed before the update can never keep serving its
pre-update result. Call it yourself only when you mutate list data outside the
normal dcupl.update() flow. It keeps a scoped list's key set, so it does not replace
update() when new matches of a seed query should join the list.
Signature:
invalidate(): voidExamples:
list.invalidate();
list.catalog.query.execute(); // recomputed from current datadestroy()
Destroys the list and cleans up resources.
Signature:
destroy(): voidExamples:
const list = dcupl.lists.create({ modelKey: 'Product' });
list.destroy();import type { DcuplList } from '@dcupl/core';
class MyComponent {
private list: DcuplList;
constructor() {
this.list = dcupl.lists.create({ modelKey: 'Product' });
}
destroy() {
this.list.destroy();
}
}on()
Subscribes to every event of the list, including reads (query_execute, fn_facets, …), plus the
instance's dcupl_updated_manually after each dcupl.update(). To
re-render when the list's result changes, use onResultChange(). See the
Events reference for all event types.
Signature:
on(callback: CallbackFunction): () => voidParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
callback |
CallbackFunction |
Yes | Function to call on changes |
Returns: () => void - Unsubscribe function
Examples:
const unsubscribe = list.on((message) => {
console.log('List changed:', message);
});unsubscribe();list.on((message) => {
if (message.type === 'result_updated') {
console.log('Result changed');
}
if (message.type === 'query_apply') {
console.log('Query was applied');
}
});const unsub1 = list.on(callback1);
const unsub2 = list.on(callback2);
// Cleanup
unsub1();
unsub2();onResultChange()
Subscribes to changes of the list's result: list.on() filtered to result_updated. The callback
fires when a query change recomputes the result (addCondition, removeGroup, clear,
applyOptions, …; not with { skipProcessing: true }) and once per dcupl.update() that can have changed the list. It never fires
for reads (items(), execute(), fn.facets(), …) or for a manual list.update(). This is the
recommended hook for re-rendering.
Signature:
onResultChange(callback: CallbackFunction): () => voidReturns: () => void - Unsubscribe function
Examples:
const unsubscribe = list.onResultChange(() => {
renderProducts(list.catalog.query.items());
});
list.catalog.query.addCondition({ operator: 'eq', attribute: 'status', value: 'active' }); // fires once
dcupl.data.upsert([{ key: 'p5', name: 'Chair', price: 80, status: 'active' }], { model: 'Product' });
await dcupl.update(); // fires once
// Later: cleanup
unsubscribe();Configuration types
DcuplListOptions
Configuration options for creating a list.
Type Definition:
type DcuplListOptions = {
modelKey: string;
listKey?: string;
listItemKeys?: string[];
query?: DcuplGlobalQueryOptions<ListItem>;
autoUpdate?: boolean;
};Properties:
| Property | Type | Required | Description |
|---|---|---|---|
modelKey |
string |
Yes | Model this list is based on |
listKey |
string |
No | Unique list identifier (auto-generated if omitted) |
listItemKeys |
string[] |
No | Scopes the list to these item keys (the key set is kept across updates; current values are served, and an item added later under one of the keys joins) |
query |
DcuplGlobalQueryOptions<ListItem> |
No | Seed query: scopes the list to the items matching it at creation (key set kept across updates, not an initial filter) |
autoUpdate |
boolean |
No | Re-run a seed query on every dcupl.update() (new matches join, items that stop matching leave); a listItemKeys list serves the same items either way, with it the list also emits list_updated on every update that affects it. Default: performance.autoUpdateLists.enabled (false) |
autoUpdate works at runtime but is missing from the dcupl.lists.create() parameter type, so
TypeScript rejects it in an object literal passed to create(). Declare the
options as DcuplListOptions (from @dcupl/common) and pass the variable, as in the examples below.
Examples:
const options: DcuplListOptions = { modelKey: 'Product' };const options: DcuplListOptions = {
modelKey: 'Product',
listKey: 'featuredProducts',
};const options: DcuplListOptions = {
modelKey: 'Product',
listItemKeys: ['p1', 'p2', 'p3'],
};const options: DcuplListOptions = {
modelKey: 'Product',
query: {
modelKey: 'Product',
queries: [{ operator: 'eq', attribute: 'status', value: 'active' }],
},
};const options: DcuplListOptions = {
modelKey: 'Product',
listKey: 'activeProducts',
query: {
modelKey: 'Product',
queries: [{ operator: 'eq', attribute: 'status', value: 'active' }],
},
autoUpdate: true,
};Auto-update pattern
Auto-update matters only for a query-scoped list: with it, every dcupl.update() that affects
the list re-runs the seed query, so a new item that matches joins the list and one that stops
matching leaves; without it, the list keeps the keys the query matched at creation (or at the last
list.update()). A listItemKeys list is the same with or without it: it always holds the current
items for exactly its keys. Unscoped lists are not affected either: every dcupl.update() drops
the caches of every affected list, so their queries always run against the current data.
Priority: dcupl.update({ autoUpdateLists }) > the list's autoUpdate >
performance.autoUpdateLists.enabled (default false). Pass autoUpdate through a
DcuplListOptions variable, as in the last example above; Real-time updates
walks through the update flow end to end.
More patterns
A list for a fixed subset takes the query or listItemKeys option (see DcuplListOptions);
for a filter that always follows the current data, call catalog.query.addCondition() on an unscoped
list instead (Catalog API). Several filter groups on one list are
covered in Query logic. These patterns have their own guides:
- Master-detail: a detail list that follows the selection in a master list
- Text search: substring and case-insensitive search, escaping user input
- Faceted search: facets with counts that follow the other filters
- Pagination & infinite scroll: paging with
start/count - Real-time updates: keeping lists in sync with incoming data