Skip to content

Commit

Permalink
Add README file to access component
Browse files Browse the repository at this point in the history
  • Loading branch information
NikitaMazur committed Jul 30, 2020
1 parent f3fa048 commit cb1b84f
Showing 1 changed file with 59 additions and 264 deletions.
323 changes: 59 additions & 264 deletions packages/access/README.md
Original file line number Diff line number Diff line change
@@ -1,287 +1,82 @@
## The goal

working with REST-api and redux, using common practice, we always create almost same actions and reducers to send HTTP request to different endpoints. This will lead to problem that our projects will always have lot of duplicated code. For example
### CheckAccessProvider
A simple provider based on the Redux library, used to store access to components and application pages

```
// action types
const FETCH_USERS_LIST = Symbol('FETCH_USERS_LIST')
const FETCH_USER = Symbol('FETCH_USER')
const SAVE_USER = Symbol('SAVE_USER')
const CREATE_USER = Symbol('CREATE_USER')
Props:

// action creators
function fetchUsersList() {
return {
type: FETCH_USERS_LIST
}
}
| Property | type | Description |
| --------------- | --------------------- | --------------------- |
| userLevelSelector | Integer | Selector that returns integer value for comparison with the access flag |

function fetchUser(id) {
return {
type: FETCH_USER,
payload: id,
}
}
Usage:

function saveUser(id, data) {
return {
type: SAVE_USER,
payload: data,
meta: {id}
}
}
```javascript
import { Provider } from 'react-redux'
import { CheckAccessProvider } from '@ds-frontend/access'

function createUser(data) {
return {
type: CREATE_USER,
payload: data,
}
}
import userLevelSelector from '...path'

// reducers
function users(state = {}, action) {
switch(action.type) {
case FETCH_USERS_LIST:
return {...state, isLoading: true}
case SAVE_USER:
return {...state, ...action.payload, isLoading: false}
}
function App(){
return (
<Provider store={store}>
<CheckAccessProvider userLevelSelector={userLevelSelector}>
// ...childrens
</CheckAccessProvider>
</Provider>
)
}
// epics
// TODO
...
```
this example does not contains error handling, caching data, authorization, options, filters ...
And basically we always copy paste this code from file to file and rename function names and constants values

```
fetchUsers
fetchBooks
fetchGroups
fetchComputers
fetchOrders
etc
```

this package will help u stop to duplicating code and start thinking about more interesting staff in your projects
Than you need to create **userLevelSelector** file. This file should describe the logic of all permissions for each role. For this we prefer to use the **reselect** library

1. REST API CRUD:
```
endpoint: /api/v1/users/:id
```javascript
import isEmpty from 'lodash/isEmpty'
import get from 'lodash/get'
import { createSelector } from 'reselect'

GET /api/v1/users/ - get users list
POST /api/v1/users/ - create new user
GET /api/v1/users/1 - get user details
PATCH /api/v1/users/1 - update user
POST /api/v1/users/1 - recreate user
PUT /api/v1/users/1 - полностью перезаписать юзера (обычно не используем)
DELETE /api/v1/users/1 - delete user
OPTIONS /api/v1/users/ - get metadata
```
export const F_PUBLIC = 2 ** 0
export const F_PROTECTED = 2 ** 1
export const F_UNAUTHORISED = 2 ** 2
export const F_CHIEF = 2 ** 52 - 1

2. endpoint CRUD
```
endpoint: /api/v1/some/custom/endpoint
```
And this basically same as previous, but it has only 1 single resource.
export const userLevelSelector = createSelector(
// base permissions
(state) => isEmpty(get(state, 'session.data')) ? F_UNAUTHORISED : F_PROTECTED,

resources will give u ability to add all data and methods to communicate with REST API almost with 1 line of code.
```
connectResource([resource])
// collect all user permissions
(...args) => args.reduce((level, flag) => level | flag, F_PUBLIC),
)

// where:
resource = {
namespace: 'internalResourceName',
endpoint: '/some/endpoint/with/:placeholders',
forceUpdates: true|false (default false),
withNavigation: true|false (default false),
reducer: 'object|paginationList|none|replace|custom function' (default 'object'),
queries: [],
}
```
*NOTE: F_CHIEF have full access to application. It should contains all flags. the value should be next exponent minus one. The maximum exponent can be 52, because the MAX_SAFE_INTEGER is (2 ** 53)*

And in props u will have next data:
```
props.internalResourceName = {
data: { /* ... resource data from API ... */ },
isLoading: true|false,
options: { /* parsed OPTIONS from API */ },
errors: null, // or object with errors { },
// actions
fetch: func, // GET request, useful when no prefetch
fetchOptions: func, // OPTIONS request
create: func, // POST request
save: func, // PATCH request
update: func, // PATCH request, alias for save
remove: func, // DELETE request
replace: func, // PUT request if you need it
setData: func, // you can update data in store manually, but please be carefull with this action
setLoading: func, // you can updates isLoading in store manually, but please be carefull with this action
setErrors: func, // you can updates errors in store manually, but please be carefull with this action
setFilters: func, // you can updates current filters in store manually, but please be carefull with this action
filters: {}, // current applied filters
}
```


## Options

all options could be defined with connectResource (on initialization level) and then u can override all options when u will call a function


#### `namespace : String` [required]

property name for resource binding. e.g. for `namespace: 'test'` you will get resource in `props.test`. And all data will be saved in store under the "test" key.


#### `endpoint : String` [optional] [default: value of namespace option]

will be set to `namespace` if ommited. resource endpoint name - where to get data from server.
can contains placeholders (see doc for `path-to-regexp` package for additional information). all you current props will be forwarded to `path-to-regexp`, plus one additional property `id` wich will be get by `idKey` (see below)

#### `forceUpdates : Boolean` [optional] [default: false]
By default resources will trigger all circle methods
1. toggle loading, clear errors, safe filters
2. send HTTP request
3. toggle loading, set errors(on catch)

In case u want just send HTTP request and then send another one for better performance it is better to set this param to false and call this methods by your own

#### `queries : Object` [optional] [default: {}]

used with list resources. representing initial query for fetch request.
* By default it will skip queries for all API requests except GET
### CheckAccess
React component (Consumer) for condition rendering

#### `reducer : String|Function` [optional] [default: 'object'}]
one of possible reducer functions that u can use.
Possible variants:
- **object**: Object.assign (prevState, nextState)
- **paginationList**: reducer for lists dataStructures `{count:int,results:[]}`
- **none**: return prevState
- **replace** return nextState
or u can define your custom function
Props:

| Property | type | Description |
| --------------- | --------------------- | --------------------- |
| access | Integer | Acess Level |
| fallback | React Element | React Componet to render if condition is `false` |
| children | Node | Will render children component if condition is `true` |

Usage:

### Examples
```javascript
import { CheckAccess } from '@ds-frontend/access'

```
class App extends Component {
componentDidmount() {
//get users
this.props.users.fetch()
//get books
this.props.books.fetch({offset: 0, limit: 25})
//get book by id
this.props.books.fetch({id: 'harry_potter'})
//but u can to more!
this.props.books.fetch({id: 'harry_potter'}, {namespace: 'test', endpoint: 'cars'}) //that will send get request to cars endpoint and save data to test in redux
or
this.props.books.setLoading(true); //set loading
this.props.books.fetch(null, {endpoint: 'users/me', reducer: 'none', forceUpdates: true})
.then((me)=>this.props.books.fetch({id: me.uuid})) //get book with same uuid as user
}
render(){...}
}
connectResource([
{
namespace: 'books',
endpoint: 'books/:id?',
queries: ['offset', 'limit'],
reducer: 'paginationList'
},
'users' //u can use even just a string in case your enpoint == namespace and all other configs are default
])(App)
```

#### connect single resource
```
connectResource({
namespace: 'books',
endpoint: 'books/:id?'
})
or
connectResource('users')
```
#### connect multiple resource
```
connectResource([{
namespace: 'books',
endpoint: 'books/:id?'
}, 'users'])
```

#### customResource
In case u need more complex logic rather then sending 1 HTTP request, but u still want to have all abilities that brings `resources`.
customResource will return HOC which will add same props as connectResource, and add 1 more function `this.props[namespace].customFetch` and this customFetch will work same as .fetch but instead of sending predefined single HTTP request it will run your own async task. So it might be very useful in your projects to have standard resource abilities + your own async job

```
function myCustomFetch(API, payload, meta) {
return new Promise(function(resolve,reject){
setTimeout(()=>resolve({succes: true}),1000)
})
}
const customConnect = customResource(myCustomFetch)
```

then u can use this HOC with your components
```
class Test extends Component {
componentDidMount(){
this.props.test.customFetch({},{<owerride configs>})
}
...
import { F_PROTECTED } from 'path/userLevelSelector'


export default function SessionText() {
return (
<CheckAccess
access={F_PROTECTED}
fallback={_ => <span>Unauthorised</span>}
>
<span>Authorised</span>
</CheckAccess>
)
}
export default customConnect({namespace: 'test', endpoint: 'test'})(Test)
//or
export default customConnect('test')(Test)
///but not
customConnect([{...}, 'test'])(Test)
```

customConnect HOC will not support array of resources because it is single resource HOC


# HOCS
This package will also provide a standard list of React Hight Order Components:
- [prefetchResources](https://github.com/django-stars/ds-frontend-resource/blob/master/src/hocs/Prefetch.md)
- [withReduxForm](https://github.com/django-stars/ds-frontend-resource/blob/master/src/hocs/WithReduxForm.md)
- [withInfinityList](https://github.com/django-stars/ds-frontend-resource/blob/master/src/hocs/WithInfinityList.md)
- [withFinalForm](https://github.com/django-stars/ds-frontend-resource/blob/master/src/hocs/WithFinalForm.md)
- navigationToProps


# Hook

## useResource
Hook that will create resource in Redux and return same props as connectResources
Accepts same configuration as connectResources

```
import { useResource } from '@ds-frontend/resource'
function MyReactComponent () {
const { data, isLoading, errors, fetch } = useResource('users/me')
useEffect(()=>{
const request = fetch()
return request.cancel
}, [])
if(isLoading) {
return 'Loading...'
}
if(errors){
return 'Oooops something went wrong please contact local police office'
}
return <UserElement user={data}/>
}
```


Since our flag depends on whether our session is active or not, a text `Authorised` will be displayed during an active session. Otherwise, the text `Unauthorised` will be displayed

0 comments on commit cb1b84f

Please sign in to comment.