Skip to content

Commit

Permalink
feat: Change factory function signature
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

```js
// old
buildList({
  name: "PAGE__SECTION--TODOS",
  methods: {
    create: data => POST("/todos", data),
    read: () => [{id: 1, title: "lorem ipsum"}],
    update: (id, data) => PATCH(`/todos/${id}`, date),
    delete: id => DELETE(`/todos/${id}`),
  }
})

// new
buildList("PAGE__SECTION--TODOS", {
  create: data => POST("/todos", data),
  read: () => [{id: 1, title: "lorem ipsum"}],
  update: (id, data) => PATCH(`/todos/${id}`, date),
  delete: id => DELETE(`/todos/${id}`),
})
```
  • Loading branch information
andreidmt committed Sep 15, 2019
1 parent 29f9271 commit 1560c7a
Show file tree
Hide file tree
Showing 15 changed files with 141 additions and 214 deletions.
151 changes: 62 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,12 @@
* [Aggregate](#aggregate)
* [Mitigate inconsistent API](#mitigate-inconsistent-api)
* [Race free](#race-free)
* [Cache](#cache)
* [It's Redux](#its-redux)
* [Install](#install)
* [Example](#example)
* [API](#api)
* [Definition](#definition)
* [Params](#params)
* [Retuns](#retuns)
* [Retuns](#retuns)
* [Internal state slice](#internal-state-slice)
* [Add to Radux](#add-to-radux)
* [Consume in container component](#consume-in-container-component)
* [Selectors](#selectors)
* [Recommendations](#recommendations)
* [Develop](#develop)
Expand All @@ -50,13 +45,9 @@

> All CRUD operations are done in sequence. If `update` is issued after `delete`, the `update` promise will wait for `delete` to finish and then do it's work.
### Cache

> `find` operations are cached based on it's signature.
### It's Redux

> Treat your state data as simple lists with common metadata helpers (isLoading, isUpdating etc.) and less boilerplate.
> Treat your state data as simple lists with common metadata helpers (isLoading, isUpdating etc.).
## Install

Expand All @@ -71,20 +62,18 @@ npm install @mutantlove/redux-list
Define a list of todos from our local API.

```js
// todos.state.js
// todos.list.js

import { buildList } from "@mutantlove/redux-list"

export const TodosList = buildList({
name: "PAGE__SECTION--TODOS",
cacheTTL: 1000,
methods: {
create: data => POST("/todos", data),
find: () => [{id: 1, title: "lorem ipsum"}],
update: (id, data) => PATCH(`/todos/${id}`, date),
delete: id => DELETE(`/todos/${id}`),
},
const TodosList = buildList("PAGE__SECTION--TODOS", {
create: data => POST("/todos", data),
read: () => [{id: 1, title: "lorem ipsum"}],
update: (id, data) => PATCH(`/todos/${id}`, date),
delete: id => DELETE(`/todos/${id}`),
})

export {TodosList}
```

Hook internal list reducers into the state store.
Expand Down Expand Up @@ -123,7 +112,7 @@ import { TodosList } from "./todos.state"
}
},
dispatch => ({
xHandleTodosFind: TodosList.find(dispatch),
xHandleTodosFind: TodosList.read(dispatch),
})
)
class TodosContainer extends React.Component {
Expand Down Expand Up @@ -152,69 +141,58 @@ export { TodosContainer }

## API

### Definition

`buildList` is the only exposed function. It prepares the reducer and CRUD actions that interface and data sources.
`buildList` is the only exposed function.

```js
import { buildList } from "@mutantlove/redux-all-is-list"

buildList({
name: "PAGE__SECTION--TODOS",
cacheTTL: 100,
methods: {
create: data => POST("/todos", data),
find: ({ offset, limit }) =>
GET("/todos", {
offset,
limit,
}),
update: (id, data) => PATCH(`/todos/${id}`, date),
delete: id => DELETE(`/todos/${id}`),
},
})
```

#### Params

Object containing:

**`*name`**`:string`

Unique name used for the redux store key. If multiple lists use the same name, an error will be thrown. This is because the list is ment to be added on the root level of the redux store. Use [BEM](http://getbem.com/naming/) for naming, ex. `{page}__{section}--{entity}`

**`methods`**`:Object`

Define list's CRUD actions and map to one or more data sources (local storage, 3rd party APIs or own API). There are only 4 actions that can be defined.

* `.create(data: Object, { isDraft: bool = false }): Promise<Object>`
* Add return obj to main `slice.items` - `id` field is required.
* Add data obj to `slice.creating` array, cleared after promise resolves.
* Toggle `slice.isCreating` flag before and after promise resolves.
* If `isDraft` is true, the method will not run. The data object will be simply added to the `slice.items` array.
* Clear cache if `cacheTTL` is set.

* `.find(...args: any[]): Promise<Object[]>`
* Replace `slice.items` contents with return array - `id` field is required in each item.
* Toggle `slice.isLoading` flag before and after promise resolves.
* Set `slice.loadDate` to the current time (Date object) after promise resolves.
* Results will be cached based on `args`. `find({offset: 10})` will be cached separately than `find()`.

* `update(id: string|number, data: Object, { isDraft: bool = false }): Promise<Object>`
* Update item in `slice.items` if exists (merge by `id`), add otherwise.
* Add item to `slice.updating` array, cleared after promise resolves.
* If `isDraft` is true, the method will not run. The data object will be simply merged or added to the `slice.items` array.
* Clear cache if `cacheTTL` is set.
buildList(
/**
* Unique name used as Redux store key. If multiple lists use the same
* name, an error will be thrown.
* This is because the list is ment to be added on the root level of
* the store.
*
* Use BEM (getbem.com/naming) for naming, ex. `{page}__{section}--{entity}`
*/
"PROFILE__LATEST--WRITTEN-ARTICLES",

/**
* Define list's CRUD actions and map to one or more data sources (local
* storage, 3rd party APIs or own API). There are only 4 actions that can
* be defined: `create`, `read`, `update` and `delete`.
*/
{
/**
* Create
*
* Redux actions will be dispatched before and after the method call.
* `${name}_CREATE_START` before and `${name}_CREATE_SUCCESS` or
* `${name}_CREATE_ERROR` after, depending if method throws an error.
*
* @param {Object} data An `id` field must be present
* @param {Object} options If called with `isDraft` option set to true,
* this method will not run. The data object will
* simply be added `slice.items`.
*
* @returns Object | Promise<Object>
*/
create: (data, options, ...rest) => {
return {
id: "uuid",
}
}

* `delete: (id: string|number): Promise`
* Delete item with `id`. Return value is ignored.
* Clear cache if `cacheTTL` is set.
read: (...rest) => {
},

**`cacheTTL`**`: number`
update: (id, data, options, ...rest) => {
},

Number of miliseconds a cached value is valid.
delete: (id, ...rest) => {
}
})
```

#### Retuns
### Retuns

Object containing:

Expand All @@ -233,13 +211,13 @@ const store = createStore(
)
```

**`create|find|update|delete`**`: (dispatch: Function): Function`
**`create|read|update|delete`**`: (dispatch: Function): Function`

Curry function that make available the store's `dispatch` to the functions in `methods`. Error will be thrown if the method is not defined in builder function's `methods` obj.
Curried function that takes the store's `dispatch`. Error will be thrown if method is not defined.

```js
@connect(mapStateToProps, dispatch => ({
xHandleTodosFind: TodosList.find(dispatch),
xHandleTodosFind: TodosList.read(dispatch),
}))
```

Expand All @@ -260,10 +238,6 @@ Curry function that make available the store's `dispatch` to the functions in `m
}
```

### Add to Radux

### Consume in container component

### Selectors

* **`.head`**`: () => Object|undefined`
Expand All @@ -281,9 +255,8 @@ Curry function that make available the store's `dispatch` to the functions in `m

## Recommendations

* Don't reuse. A list should be used once per page/section.
* Group all lists per page into a separate file to avoid variable name collision.
* Don't store data locally, data lives in the database - that's the real application state.
* A list should be used once per page/section.
* Don't store data locally, data lives in the database - the real application state.

## Develop

Expand Down
24 changes: 9 additions & 15 deletions src/create/create.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@ import { buildList } from ".."

test("Create", t => {
// WHAT TO TEST
const todoList = buildList({
name: "CREATE_TODOS",
methods: {
create: (data, options, other) => ({
id: 1,
...data,
options,
other,
}),
},
const todoList = buildList("CREATE_TODOS", {
create: (data, options, other) => ({
id: 1,
...data,
options,
other,
}),
})

// Redux store
Expand Down Expand Up @@ -97,11 +94,8 @@ test("Create", t => {

test("Create - multiple", t => {
// WHAT TO TEST
const todoList = buildList({
name: "CREATE-MULTIPLE_TODOS",
methods: {
create: items => items.map((item, index) => ({ id: index, ...item })),
},
const todoList = buildList("CREATE-MULTIPLE_TODOS", {
create: items => items.map((item, index) => ({ id: index, ...item })),
})

// Redux store
Expand Down
25 changes: 11 additions & 14 deletions src/create/create__error.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,17 @@ class RequestError extends Error {

test("Create - error", t => {
// WHAT TO TEST
const todoList = buildList({
name: "CREATE-ERROR_TODOS",
methods: {
read: () => [],
create: ({ name }) => {
return name === "throw"
? Promise.reject(
new RequestError("Something something API crash", {
body: { validationData: "from server" },
status: 409,
})
)
: Promise.resolve({ id: 1, name })
},
const todoList = buildList("CREATE-ERROR_TODOS", {
read: () => [],
create: ({ name }) => {
return name === "throw"
? Promise.reject(
new RequestError("Something something API crash", {
body: { validationData: "from server" },
status: 409,
})
)
: Promise.resolve({ id: 1, name })
},
})

Expand Down
9 changes: 3 additions & 6 deletions src/delete/delete.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import { buildList } from ".."

test("Delete", t => {
// WHAT TO TEST
const todoList = buildList({
name: "DELETE_TODOS",
methods: {
read: () => [{ id: 1, name: "lorem ipsum" }, { id: 2, name: "foo bar" }],
delete: (id, testRest) => ({ id, testRest }),
},
const todoList = buildList("DELETE_TODOS", {
read: () => [{ id: 1, name: "lorem ipsum" }, { id: 2, name: "foo bar" }],
delete: (id, testRest) => ({ id, testRest }),
})

// Redux store
Expand Down
9 changes: 3 additions & 6 deletions src/delete/delete__different-id-in-result.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import { buildList } from ".."

test("Delete - different id in response", t => {
// WHAT TO TEST
const todoList = buildList({
name: "DELETE-ERROR-DIFFERENT-ID_TODOS",
methods: {
read: () => [{ id: 1, name: "build gdpr startup" }, { id: 2 }],
delete: () => Promise.resolve({ id: 1 }),
},
const todoList = buildList("DELETE-ERROR-DIFFERENT-ID_TODOS", {
read: () => [{ id: 1, name: "build gdpr startup" }, { id: 2 }],
delete: () => Promise.resolve({ id: 1 }),
})

// Redux store
Expand Down
9 changes: 3 additions & 6 deletions src/delete/delete__no-id-in-result.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import { buildList } from ".."

test("Delete - id not in response", t => {
// WHAT TO TEST
const todoList = buildList({
name: "DELETE-ERROR-NO-ID_TODOS",
methods: {
read: () => [{ id: 1, name: "build gdpr startup" }, { id: 2 }],
delete: () => ({ name: "I dont know who I am :(" }),
},
const todoList = buildList("DELETE-ERROR-NO-ID_TODOS", {
read: () => [{ id: 1, name: "build gdpr startup" }, { id: 2 }],
delete: () => ({ name: "I dont know who I am :(" }),
})

// Redux store
Expand Down
25 changes: 11 additions & 14 deletions src/delete/delete__throw.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,17 @@ class RequestError extends Error {

test("Delete - error", t => {
// WHAT TO TEST
const todoList = buildList({
name: "DELETE-ERROR_TODOS",
methods: {
read: () => [{ id: 1, name: "build gdpr startup" }, { id: 2 }],
delete: id => {
return id === 2
? Promise.reject(
new RequestError("Something something API crash", {
body: { message: "resource not found" },
status: 404,
})
)
: { id: 1 }
},
const todoList = buildList("DELETE-ERROR_TODOS", {
read: () => [{ id: 1, name: "build gdpr startup" }, { id: 2 }],
delete: id => {
return id === 2
? Promise.reject(
new RequestError("Something something API crash", {
body: { message: "resource not found" },
status: 404,
})
)
: { id: 1 }
},
})

Expand Down
6 changes: 3 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ const hasKey = key => obj => Object.prototype.hasOwnProperty.call(obj, key)
/**
* List factory function
*
* @param {Object} props List props
* @param {string} props.name Unique name so actions dont overlap
* @param {string} name Unique name so actions dont overlap
* @param {Object} methods Object with CRUD method
*
* @return {Object}
*/
const buildList = ({ name, methods = {} }) => {
const buildList = (name, methods = {}) => {
if (hasKey(name)(collections)) {
throw new Error(`ReduxList: List with name "${name}" already exists`)
}
Expand Down
Loading

0 comments on commit 1560c7a

Please sign in to comment.