Home

wq/model.js

wq version: 1.0 1.1 1.2-beta
Docs > wq.app: Modules

Note: This documentation is for wq 1.2, which has not yet been finalized.

@wq/model

@wq/model

@wq/model is a wq.app module providing a simple API for working with lists, or collections of similar objects. It uses @wq/store to retrieve the underlying JSON data from e.g. a REST API.

As of wq.app 1.2, @wq/model is based on Redux-ORM and provides similar querying capabilities, while still supporting asyncronous APIs for paginating through data from the server.

Installation

wq.app for PyPI

@wq/app for npm

python3 -m venv venv      # create virtual env (if needed)
. venv/bin/activate       # activate virtual env
python3 -m pip install wq # install wq framework (wq.app, wq.db, etc.)
# pip install wq.app      # install wq.app only
npm install @wq/app       # install all @wq/app deps including @wq/model
# npm install @wq/model   # install only @wq/model and deps

API

When using @wq/app, @wq/model instances are automatically defined for all "list" pages in the wq configuration object. When used directly, @wq/model is typically imported as model, though any local variable name can be used.

wq.app for PyPI

@wq/app for npm

// @wq/app usage
define(['wq/app', './config', ...], function(app, config, ...) {
   app.init(config).then(...);
   var items = app.models.item;
});

// Direct usage
define(['wq/model', ...], function(model, ...) {
   var items = model({'url': 'items', 'name': 'item'});
});
// @wq/app usage
import app from '@wq/app';
import config from './config';
app.init(config).then(...);
const items = app.models.item;

// Direct usage
import model, { Model } from '@wq/model';
const items = model({'url': 'items', 'name': 'item'}); 
// const items = new Model(...);

Initialization

The model constructor accepts a page configuration object that configures the name, data source, and caching strategy.

name purpose
name Required in wq.app 1.2. Unique name for the model. @wq/app already provides this for configured models, but if you are using @wq/model directly you will need to specify it.
url URL path for the REST API endpoint corresponding to this model (relative to the @wq/store service URL).
store The @wq/store instance to use for the model. This defaults to the main instance if not set.
cache Caching strategy (per the wq configuration object).
idCol New in wq.app 1.2. Attribute to use as primary identifier for items in collection. (Defaults to "id"). Note that when working with a wq.db-based REST API, the JSON objects will always have an id attribute that maps to the actual identifier column. (So, it is usually not necessary to set idCol on the client).
form List of editable fields (other than id) as described in the wq configuration object.
functions A collection of functions defining computable attributes that can be applied to items in the model.
filter_fields New in wq.app 1.2. List of read-only server-defined fields that should be filterable. Fields defined in form and functions do not need to be listed here.
filter_ignore New in wq.app 1.2. List of fields or URL parameters to ignore when filtering. This can be useful when e.g. defining custom list views that change based on URL parameters.
query Deprecated in wq.app 1.2. The @wq/store query to use when retrieving data for the model. This is almost always {"url": url} and so it is usually better to just use the url option instead.

The following definitions of myModel are essentially equivalant:

// Formal usage
var myModel = new Model({"name": "item", "url": "items"});

// Shortcut constructor
var myModel = model({"name": "item", "url": "items"});

// wq configuration
var wqConfig = {
    "pages": {
        "item": {
            "name": "item",
            "url": "items",
            "list": true
        }
    }
}
app.init(wqConfig);
var myModel = app.models.item;

When using @wq/model directly, it is possible to define a model without a URL, e.g. for storing local data with no server representation. There is no equivalent for this when using @wq/app configuration, as all models are defined as REST endpoints.

// Formal usage
var myModel = new Model({"name": "bookmark"});

// Shortcut constructor
var myModel = model({"name": "bookmark"});

// Even shorter
var myModel = model("bookmark");

Query APIs

[model].objects

New in wq.app 1.2. Returns a Redux-ORM queryset for the model based on the current Redux state. Note that this API only works with local data that has already been retrieved from the server. To run queries without concern for whether the data already exists locally, use one of the asynchronous query methods below.

var items = myModel.objects.all()
                 .filter({"type_id": 3})
                 .orderBy("name");

The name objects is inspired by equivalent attribute for Django models.

[model].load()

Asynchronously loads the (local) contents of the model into memory. If the local cache has not already been populated, load() automatically retrieves it from the server. The resolved value will be structured as follows:

{
   "list": [...]   // First page of data
   "count": 15,    // Total number of items in list (including server-only items)
   "pages": 1,     // Number of server-paginated data pages
   "per_page": 50  // Number of items per page
}

Note that the values for pages, count, and per_page will be set by the REST API if the server is wq.db or a compatible web service.

This function (and the related query functions below) all return a Promise that will resolve to the requested data. If you are using @wq/app for npm (or are only targeting modern browsers), the async/await keywords will help streamline your code.

wq.app for PyPI
@wq/app for npm
myModel.load().then(function(data) {
    data.list.forEach(function(item) {
        console.log(item.id, item.label);
    });
});
const data = await myModel.load();
data.list.forEach(item => {
    console.log(item.id, item.label);
});

[model].info()

info() returns a Promise that resolves to a value with the same structure as load() but without the actual list of data.

wq.app for PyPI
@wq/app for npm
myModel.info().then(function(info) {
    console.log("Total Items:", info.count);
});
const info = await myModel.info();
console.log("Total Items:", info.count);

[model].page(page_num)

Like load(), but retrieves the items in the list at the specified page number (starting with page 1). If the cache setting is "first_page" or "all", page(1) is effectively equivalent to load(). In most other cases, page() will generate a network request to retrieve the data from the server, and the result will not be stored locally.

wq.app for PyPI
@wq/app for npm
myModel.page(4).then(function(data) {
    data.list.forEach(function(item) {
        console.log(item.id, item.label);
    });
});
const data = await myModel.page(4);
data.list.forEach(item => {
    console.log(item.id, item.label);
});

[model].find(value, [localOnly])

find() can be used to asynchronously retrieve a single item from the model based on the primary key (usually "id"). If not all of the data for the model is stored locally (i.e. cache is not "all"), then find() will automatically query the server for any items not found locally. This behavior can be disabled by setting localOnly to true.

wq.app for PyPI
@wq/app for npm
myModel.find(27).then(function(item) {
    console.log(item.id, item.label);
});
const item = await myModel.find(27);
console.log(item.id, item.label);

Changed in wq.app 1.2: find() no longer accepts a custom id column as the second argument. If the primary key is not "id", specify idCol when defining the model.

[model].filter(filter[, any[, localOnly]])

filter() asynchronously retrieves all objects that match the specified filter, which should be key-value mapping of one or more fields to filter on. Fields can be existing fields on the item in the list, or the names of attribute functions provided to the model constructor. The any argument specifies whether to return items matching any of the filter values (true) or only those matching all of the filter values (false, default).

If not all of the data for the model is stored locally (i.e. cache is not "all"), then filter() will always query the server even if some items might be found locally. This behavior can be disabled by setting localOnly to true.

wq.app for PyPI
@wq/app for npm
// Filter on existing field
myModel.filter({'type_id': 3}).then(function(type3items) {
    type3items.forEach(function(item) {
        console.log(item.id, item.label);
    });
});

// Filter on existing field, match multiple values
myModel.filter({'type_id': [1, 2]}).then(function(type1and2items) {
    type1and2items.forEach(function(item) {
        console.log(item.id, item.label);
    });
});

// Filter on predicate function (new in wq.app 1.2)
myModel.filter(function(item) {
    return item.size > 100;
}).then(function(bigItems) {
    bigItems.forEach(function(item) {
        console.log(item.id, item.label);
    });
});

// Filter on a predefined computed field
var functions = {
    'big': function(item) {
        return item.size > 100;
    }
};
var myModel = model({'name': 'item', 'url': 'items', 'functions': functions});
myModel.filter({'big': true}).then(function(bigItems) {
    bigItems.forEach(function(item) {
        console.log(item.id, item.label);
    });
});
// Filter on existing field
const type3items = await myModel.filter({'type_id': 3});
type3items.forEach(item => {
    console.log(item.id, item.label);
});

// Filter on existing field, match multiple values
const type1and2items = await myModel.filter({'type_id': [1, 2]});
type1and2items.forEach(function(item) {
     console.log(item.id, item.label);
});

// Filter on predicate function (new in wq.app 1.2)
const bigItems = await myModel.filter(item => item.size > 100);
bigItems.forEach(function(item) {
     console.log(item.id, item.label);
});

// Filter on a predefined computed field
const functions = {
    big(item) {
        return item.size > 100;
    }
};
const myModel = model({'name': 'item', 'url': 'items', functions});
const bigItems = await myModel.filter({'big': true});
bigItems.forEach(item => {
    console.log(item.id, item.label);
});

Changed in wq.app 1.2:. [model].filter() is now a wrapper for Redux-ORM's filter(), which provides additional features such as predicate function filters and better indexing. However, note that Redux-ORM uses strict equality when comparing object attributes. In wq.app 1.1 and earlier, {'type_id': '3'} and {'type_id': 3} returned the same result, whereas in wq.app 1.2 they are different.

[model].forEach(callback, thisarg)

[model].forEach() mimics Array.prototype.forEach to provide a simple way to iterate over all values in the local (first page) of the list. Note that this function is asynchronous, unlike a "real" forEach loop.

wq.app for PyPI
@wq/app for npm
// Using load()
myModel.load().then(function(data) {
    data.list.forEach(function(item) {
        console.log(item.id, item.label);
    });
    nextThing(); // This will execute after loop is done
});

// Using forEach() with then()
myModel.forEach(function(item) {
    console.log(item.id, item.label);
}).then(function() {
    nextThing();
});

// Using forEach() without then()
myModel.forEach(function(item) {
    console.log(item.id, item.label);
});
nextThing();  // This will happen before forEach is done!
// Using load()
const data = await myModel.load();
data.list.forEach(item => {
    console.log(item.id, item.label);
});
nextThing(); // This will execute after loop is done

// Using forEach() with await
await myModel.forEach(function(item) {
    console.log(item.id, item.label);
});
nextThing();

// Using forEach() without await
myModel.forEach(function(item) {
    console.log(item.id, item.label);
});
nextThing();  // This will happen before forEach is done!

[model].prefetch()

prefetch() prefetches all the local data pages in the list. It's usually important to do this whenever the application starts up. Note that @wq/app includes a prefetchAll() method that can automatically prefetch data for all registered models.

[model].unsyncedItems(withData)

Returns a list of all pending form submissions in the outbox that are associated with this model. If withData is true, the full form submissions will be loaded (including any binary attachments). withData is false by default.

Update APIs

[model].create(item)

New in wq.app 1.2. Create a new record locally. If no id is specified, one will be generated automatically by Redux-ORM. Note that @wq/outbox should be used if the change is intended to be reflected on the server.

[model].update(items)

update() updates the locally stored list with new and/or updated items. If ids already exist in the local store, the corresponding records are updated. Otherwise, new records are created.

var newItem = {'id': 35, 'name': "New Item"};
var items = [newItem];
myModel.update(items);

Note that [model].update() is not designed to automatically publish local changes to a remote database. Instead, @wq/outbox can be used to handle form submissions and other server-bound updates. By default, @wq/outbox does not update the local model until the form is successfully synced to the server. As of wq.app 1.2, @wq/outbox can also optimistically apply the local update immediately, and then sync to the server.

Changed in wq.app 1.2: update() no longer accepts a custom id column as the second argument. If the primary key is not "id", specify idCol when defining the model.

[model].fetchUpdate(params)

fetchUpdate() asynchronously retrieves and applies an update to a locally cached model. The params object will be added to the list URL to request a partial update from the server.

// Assuming server supports query "/items?since=-2h"
myModel.fetchUpdate({'since': '-2h'}};

Changed in wq.app 1.2: fetchUpdate() no longer accepts a custom id column as the second argument. If the primary key is not "id", specify idCol when defining the model.

[model].remove(id)

remove() deletes a single item from the locally cached model.

myModel.remove(12)

Changed in wq.app 1.2: remove() no longer accepts a custom id column as the second argument. If the primary key is not "id", specify idCol when defining the model.

[model].overwrite(items)

Completely replace the current locally stored data with a new set of items.

// Empty local cache
myModel.overwrite([]);

[model].dispatch(type, payload[, meta])

Constructs and immediately dispatches a Redux action appropriate for the model. The type argument will be expanded to ORM_{model}_{type}. The built-in reducer recognizes the following actions:

name effect
ORM_{model}_CREATE Create a new item with the specified object. If there is no id attribute, one will be generated automatically by Redux-ORM. Called internally by [model].create()
ORM_{model}_UPDATE Upsert (update or insert) an array of items into the model. Called internally by [model].update().
ORM_{model}_DELETE Delete the item with the specified id. Called internally by [model].remove().
ORM_{model}_OVERWRITE Replace the entire dataset with a new array of items. Called internally by [model].overwrite().
myModel.dispatch('CREATE', {"name": "New Item"});
myModel.dispatch('UPDATE', [{"id": 35, "name": "Updated Item"}]);
myModel.dispatch('DELETE', 12);
myModel.dispatch('OVERWRITE', []);