Skip to content

Commit

Permalink
assign temporary ids, then update from server
Browse files Browse the repository at this point in the history
(fixes #86)
  • Loading branch information
sheppard committed Jul 18, 2019
1 parent 6580895 commit 2b75917
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 24 deletions.
12 changes: 8 additions & 4 deletions packages/model/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,16 +366,20 @@ Returns a list of all pending form submissions in the outbox that are associated

### 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 updated items, using the model primary key to differentiate. Any items that aren't found in the list will be appended.
`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.

```javascript
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 sync changes back to the server. The typical workflow (configured automatically by [@wq/app]) is to have each `<form>` submission be processed by [@wq/outbox], which will sync the form data to the server and then update any local models with the newly saved data.
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.
Expand Down Expand Up @@ -408,13 +412,13 @@ Completely replace the current locally stored data with a new set of items.
myModel.overwrite([]);
```
#### `[model].dispatch(type, payload)`
#### `[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. (Note that this action is not currently used by @wq/app, as IDs are always generated by the server).
`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()`.
Expand Down
75 changes: 75 additions & 0 deletions packages/model/src/__tests__/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ var itemtypes = model({
store: ds,
cache: 'all'
});
var localmodel = model({
name: 'localmodel',
store: ds
});

ds.init({
service: 'http://localhost:8080/tests',
defaults: {
Expand Down Expand Up @@ -107,3 +112,73 @@ test('filter by boolean & non-boolean', async () => {
expect(types1).toHaveLength(1);
expect(types1).toEqual(types2);
});

test('create', async () => {
await localmodel.overwrite([]);

// Update is really upsert
await localmodel.update([
{ id: 1, label: 'Test 1' },
{ id: 2, label: 'Test 2' }
]);
expect(await localmodel.info()).toEqual({
count: 2,
pages: 1,
per_page: 2
});

await localmodel.create({ label: 'Test 3' });
expect(await localmodel.load()).toEqual({
count: 3,
pages: 1,
per_page: 3,
list: [
{ id: 3, label: 'Test 3' },
{ id: 2, label: 'Test 2' },
{ id: 1, label: 'Test 1' }
]
});
});

test('update', async () => {
await localmodel.overwrite([]);
await localmodel.create({ id: 1, label: 'Test 1' });
await localmodel.update([{ id: 1, label: 'Update Test 1' }]);
expect(await localmodel.info()).toEqual({
count: 1,
pages: 1,
per_page: 1
});
expect(await localmodel.find(1)).toEqual({
id: 1,
label: 'Update Test 1'
});
});

test('update - change ID', async () => {
await localmodel.overwrite([]);
await localmodel.create({ id: 'local-1', label: 'Test 1' });
await localmodel.dispatch(
'UPDATE',
{ id: 1234, label: 'Update Test 1' },
{ currentId: 'local-1' }
);
expect(await localmodel.load()).toEqual({
count: 1,
pages: 1,
per_page: 1,
list: [{ id: 1234, label: 'Update Test 1' }]
});
});

test('delete', async () => {
await localmodel.overwrite([]);
await localmodel.create({ id: 1, label: 'Test 1' });
await localmodel.remove(1);
expect(await localmodel.load()).toEqual({
count: 0,
pages: 1,
per_page: 0,
list: []
});
});
66 changes: 54 additions & 12 deletions packages/model/src/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const _orms = {};

const CREATE = 'CREATE',
UPDATE = 'UPDATE',
SUCCESS = 'SUCCESS',
DELETE = 'DELETE',
OVERWRITE = 'OVERWRITE';

Expand Down Expand Up @@ -40,29 +41,45 @@ class ORMWithReducer extends ORM {
if (!cls) {
return session.state;
}
const currentCount = cls.count();
let updateCount;

switch (actName) {
case CREATE: {
cls.create(action.payload);
updateCount = true;
break;
}
case UPDATE: {
case UPDATE:
case SUCCESS: {
const items = Array.isArray(action.payload)
? action.payload
: [action.payload];
items.forEach(item => cls.upsert(item));
const meta = session._modelmeta.withId(cls.modelName);
if (meta) {
var update = { count: meta.count + 1 };
if (meta.pages === 1 && meta.per_page === meta.count) {
update.per_page = update.count;

if (
action.meta &&
action.meta.currentId &&
action.meta.currentId != items[0].id
) {
const exist = cls.withId(action.meta.currentId);
if (exist) {
// See redux-orm #176
cls.create({
...exist.ref,
id: items[0].id
});
exist.delete();
}
meta.update(update);
}
items.forEach(item => cls.upsert(item));

updateCount = true;

break;
}
case DELETE: {
cls.withId(action.payload).delete();
updateCount = true;
break;
}
case OVERWRITE: {
Expand All @@ -77,6 +94,26 @@ class ORMWithReducer extends ORM {
}
}

if (updateCount) {
const meta = session._modelmeta.withId(cls.modelName);
if (meta) {
// Use delta in case server count != local count.
const countChange = cls.count() - currentCount,
update = { count: meta.count + countChange };
if (meta.pages === 1 && meta.per_page === meta.count) {
update.per_page = update.count;
}
meta.update(update);
} else {
session._modelmeta.create({
id: cls.modelName,
pages: 1,
count: cls.count(),
per_page: cls.count()
});
}
}

return session.state;
}
}
Expand Down Expand Up @@ -211,11 +248,14 @@ class Model {
return `${this.orm.prefix}_${this.name.toUpperCase()}_${type}`;
}

dispatch(type, payload) {
dispatch(type, payload, meta) {
const action = {
type: this.expandActionType(type),
payload: payload
};
if (meta) {
action.meta = meta;
}
return this.store.dispatch(action);
}

Expand Down Expand Up @@ -426,16 +466,18 @@ class Model {
return data.list;
}

// Create new item
async create(object) {
this.dispatch(CREATE, object);
}

// Merge new/updated items into list
async update(update, idCol) {
if (idCol) {
throw new Error(
'Usage: update(items). To customize id attr use config.idCol'
);
}
if (!Array.isArray(update)) {
throw new Error('Data is not an array!');
}
return this.dispatch(UPDATE, update);
}

Expand Down
8 changes: 4 additions & 4 deletions packages/outbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

**@wq/outbox** is a [wq.app] module providing an offline-cabable "outbox" of unsynced form entries for submission to a web service. @wq/outbox integrates with [@wq/store] to handle offline storage, and with [@wq/model] for managing collections of editable objects.

By default, @wq/outbox is designed to sync form submissions to the server *before* reflecting the same changes in the local @wq/model state. Accordingly, [@wq/app] is configured to show unsynced outbox records at the top of model list views and/or in a separate screen. As of wq.app 1.2, it is also possible to configure @wq/outbox to apply all changes to the local @wq/model state *immediately*, with syncing happening entirely in the background.
By default, @wq/outbox does not apply changes to the local @wq/model state until *after* form submissions are succesfully synced to the server. Accordingly, [@wq/app] is configured to show unsynced outbox records at the top of model list views and/or in a separate screen. As of wq.app 1.2, it is also possible to configure @wq/outbox to optimistically apply @wq/model changes *immediately*, with syncing happening in the background.

As of wq.app 1.2, @wq/outbox is based on [Redux Offline], and leverages its strategies for detecting network state and retrying failed submissions. Most notably, Redux Offline schedules sync attempts automatically, whereas @wq/outbox in wq.app 1.1 and earlier relied on @wq/app to manage the sync interval.

Expand Down Expand Up @@ -196,12 +196,12 @@ When `applyState` is set to `"IMMEDIATE"`, form submissions are reflected in the

Form Type | Submit Action | Commit Action | Rollback Action
----------|---------------|---------------|------------------
@wq/model (POST, PUT) | `ORM_{model}_UPDATE`* | `ORM_{model}_SUCCESS` | `ORM_{model}_ERROR`
@wq/model (POST, PUT) | `ORM_{model}_UPDATE`* | `ORM_{model}_SUCCESS`* | `ORM_{model}_ERROR`
@wq/model (DELETE) | `ORM_{model}_DELETE`* | `ORM_{model}_DELETESUCCESS` | `ORM_{model}_DELETEERROR`

Note that if the request fails, there is currently no code to process the Rollback Action (e.g. to revert the local change).
If the record does not already have a server-assigned ID, a temporary local id will be generated. When `ORM_{model}_SUCCESS` is dispatched, the local record will be updated with the new ID from the server.

> FIXME: Still need to define how local IDs are assigned and how they are updated once the item is synced.
Note that if the request fails, there is currently no code to process the Rollback Action (e.g. to revert the local change).

###### applyState = LOCAL_ONLY

Expand Down
Loading

0 comments on commit 2b75917

Please sign in to comment.