I have the following code:
export const myEpic = (action$, store) =>
action$.ofType("SOME_ACTION")
.switchMap(action => {
const {siteId, selectedProgramId} = action;
const state = store.getState();
const siteProgram$ = Observable.fromPromise(axios.get(`/url/${siteId}/programs`))
.catch(error =>{
return Observable.of({
type: 'PROGRAM_FAILURE'
error
});
});
const programType$ = Observable.fromPromise(axios.get('url2'))
.catch(error =>{
return Observable.of({
type: "OTHER_FAILURE",
error
});
});
so far so good, when there is an error I catch it, and (maybe this is wrong) map it to an action (indicating something failed).
now the question begins, I have another observable which is the result of the zip operator of the two observables from above:
const siteProgram$result$ = Observable.zip(siteProgram$, programType$)
.map(siteProgramsAndProgramTypes => siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper(siteProgramsAndProgramTypes, siteId));
the problem is that I still get to this observable as if everything is fine.
is there a way to "understand" that one of the "zipped" observables errored and then not get to the "next" of siteProgram$result$.
I think I am missing something trivial...
I don't want to have to perform this check:
const siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper = (siteProgramsAndProgramTypesArray, siteId) => {
const [programsResponse, programTypesResponse] = siteProgramsAndProgramTypesArray;
if (programsResponse.error || programTypesResponse.error){
return {
type: 'GENERAL_ERROR',
};
}
everytime I have an observable which is a result of an operator on other observable that might have errored.
in pure rxjs (not in redux observable) I think I could subscribe to it passing it an object
{
next: val => some logic,
error: err => do what ever I want :) //this is what I am missing in redux observable,
complete: () => some logic
}
// some more logic
return Observable.concat(programType$Result$, selectedProgramId$, siteProgram$result$);
What is the right way to attack this in redux observable?
Thanks.
Here is a detailed example with an API wrapper to facilitate what you're trying to achieve.
The gist is available on GitHub here
Here is the API wrapper which wraps Observable.ajax and lets you dispatch single actions or array of actions and handles both XHR and Application level generated errors that stem from the requests made with Observable.ajax
import * as Rx from 'rxjs';
import queryString from 'query-string';
/**
* This function simply transforms any actions into an array of actions
* This enables us to use the synthax Observable.of(...actions)
* If an array is passed to this function it will be returned automatically instead
* Example: mapObservables({ type: ACTION_1 }) -> will return: [{ type: ACTION_1 }]
* Example2: mapObservables([{ type: ACTION_1 }, { type: ACTION_2 }]) -> will return: [{ type: ACTION_1 }, { type: ACTION_2 }]
*/
function mapObservables(observables) {
if (observables === null) {
return null;
} else if (Array.isArray(observables)) {
return observables;
}
return [observables];
}
/**
* Possible Options:
* params (optional): Object of parameters to be appended to query string of the uri e.g: { foo: bar } (Used with GET requests)
* headers (optional): Object of headers to be appended to the request headers
* data (optional): Any type of data you want to be passed to the body of the request (Used for POST, PUT, PATCH, DELETE requests)
* uri (required): Uri to be appended to our API base url
*/
function makeRequest(method, options) {
let uri = options.uri;
if (method === 'get' && options.params) {
uri += `?${queryString.stringify(options.params)}`;
}
return Rx.Observable.ajax({
headers: {
'Content-Type': 'application/json;charset=utf-8',
...options.headers,
},
responseType: 'json',
timeout: 60000,
body: options.data || null,
method,
url: `http://www.website.com/api/v1/${uri}`,
// Most often you have a fixed API url so we just append a URI here to our fixed URL instead of repeating the API URL everywhere.
})
.flatMap(({ response }) => {
/**
* Here we handle our success callback, anyt actions returned from it will be dispatched.
* You can return a single action or an array of actions to be dispatched eg. [{ type: ACTION_1 }, { type: ACTION_2 }].
*/
if (options.onSuccess) {
const observables = mapObservables(options.onSuccess(response));
if (observables) {
// This is only being called if our onSuccess callback returns any actions in which case we have to dispatch them
return Rx.Observable.of(...observables);
}
}
return Rx.Observable.of();
})
.catch((error) => {
/**
* This if case is to handle non-XHR errors gracefully that may be coming from elsewhere in our application when we fire
* an Observable.ajax request
*/
if (!error.xhr) {
if (options.onError) {
const observables = mapObservables(options.onError(null)); // Note we pass null to our onError callback because it's not an XHR error
if (observables) {
// This is only being called if our onError callback returns any actions in which case we have to dispatch them
return Rx.Observable.of(...observables);
}
}
// You always have to ensure that you return an Observable, even if it's empty from all your Observables.
return Rx.Observable.of();
}
const { xhr } = error;
const { response } = error.xhr;
const actions = [];
const resArg = response || null;
let message = null;
if (xhr.status === 0) {
message = 'Server is not responding.';
} else if (xhr.status === 401) {
// For instance we handle a 401 here, if you use react-router-redux you can simply push actions here to your router
actions.push(
replace('/login'),
);
} else if (
response
&& response.errorMessage
) {
/*
* In this case the errorMessage parameter would refer to SampleApiResponse.json 400 example
* { "errorMessage": "Invalid parameter." }
*/
message = response.errorMessage;
}
if (options.onError) {
// Here if our options contain an onError callback we can map the returned Actions and push them into our action payload
mapObservables(options.onError(resArg)).forEach(o => actions.push(o));
}
if (message) {
actions.push(showMessageAction(message));
}
/**
* You can return multiple actions in one observable by adding arguments Rx.Observable.of(action1, action2, ...)
* The actions always have to have a type { type: 'ACTION_1' }
*/
return Rx.Observable.of(...actions);
});
}
const API = {
get: options => makeRequest('get', options),
post: options => makeRequest('post', options),
put: options => makeRequest('put', options),
patch: options => makeRequest('patch', options),
delete: options => makeRequest('delete', options),
};
export default API;
Here are the actions, action creators and epics:
import API from 'API';
const FETCH_PROFILE = 'FETCH_PROFILE';
const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
const FETCH_PROFILE_ERROR = 'FETCH_PROFILE_ERROR';
const FETCH_OTHER_THING = 'FETCH_OTHER_THING';
const FETCH_OTHER_THING_SUCCESS = 'FETCH_OTHER_THING_SUCCESS';
const FETCH_OTHER_THING_ERROR = 'FETCH_OTHER_THING_ERROR';
function fetchProfile(id) {
return {
type: FETCH_PROFILE,
id,
};
}
function fetchProfileSuccess(data) {
return {
type: FETCH_PROFILE_SUCCESS,
data,
};
}
function fetchProfileError(error) {
return {
type: FETCH_PROFILE_ERROR,
error,
};
}
function fetchOtherThing(id) {
return {
type: FETCH_OTHER_THING,
id,
};
}
const fetchProfileEpic = action$ => action$.
ofType(FETCH_PROFILE)
.switchMap(({ id }) => API.get({
uri: 'profile',
params: {
id,
},
/*
* We could also dispatch multiple actions using an array here you could dispatch another API request if needed
* We can redispatch another action to fire another epic if we want also.
* In both onSuccess and onError you can return a single action, an array of actions or null
* Note here we fire fetchOtherThing(data.someOtherThingId) which will trigger our fetchOtherThingEpic!
* In this case the data parameter would refer to SampleApiResponse.json 200 example
* { firstName: "John", lastName: "Doe" }
*/
onSuccess: ({ data }) => [fetchProfileSuccess(data), fetchOtherThing(data.someOtherThingId)],
onError: error => fetchProfileError(error),
})
const fetchOtherThingEpic = action$ => action$.
ofType(FETCH_OTHER_THING)
.switchMap(({ id }) => API.get({
uri: 'other-thing',
params: {
id,
},
onSuccess: ...
onError: ...
});
Here is a data sample that works with the examples shown above:
/*
* If possible, you should standardize your API response which will make error/data handling a lot easier on the client side
* Note, this data format is to work with the example code above
*/
/**
* Status code: 401
* This error would be caught in the .catch() method of our API wrapper
*/
{
"errorMessage": "Please login to perform this operation",
"data": null,
}
/**
* Status code: 400
* This error would be caught in the .catch() method of our API wrapper
*
*/
{
"errorMessage": "Invalid parameter.",
"data": null
}
/**
* Status code: 200
* This would be passed to our onSuccess function specified in our API options
*/
{
"errorMessage": 'Please login to perform this operation',
"data": {
"firstName": "John",
"lastName": "Doe"
}
}
Related
I have records in strapi. I am using strapi content API. In my front-end, I need to display only 2 records randomly. For limiting, I have used limit query from content API. But random fetching what keyword I need to use. The official documentation doesn't provide any details regarding this - https://strapi.io/documentation/v3.x/content-api/parameters.html#available-operators
There's no official Strapi API parameter for random. You have to implement your own. Below is what I've done previously, using Strapi v3:
1 - Make a service function
File: api/mymodel/services/mymodel.js
This will contain our actual random query (SQL), and wrapping it in a service is handy because it can be used in many places (cron jobs, inside other models, etc).
module.exports = {
serviceGetRandom() {
return new Promise( (resolve, reject) => {
// There's a few ways to query data.
// This example uses Knex.
const knex = strapi.connections.default
let query = knex('mydatatable')
// Add more .select()'s if you want other fields
query.select('id')
// These rules enable us to get one random post
query.orderByRaw('RAND()')
query.limit(1)
// Initiate the query and do stuff
query
.then(record => {
console.log("getRandom() record: %O", record[0])
resolve(record[0])
})
.catch(error => {
reject(error)
})
})
}
}
2 - Use the service somewhere, like a controller:
File: api/mymodel/controllers/mymodel.js
module.exports = {
//(untested)
getRandom: async (ctx) => {
await strapi.services.mymodel.serviceGetRandom()
.then(output => {
console.log("getRandom output is %O", output.id)
ctx.send({
randomPost: output
}, 200)
})
.catch( () => {
ctx.send({
message: 'Oops! Some error message'
}, 204) // Place a proper error code here
})
}
}
3 - Create a route that points to this controller
File: api/mymodel/config/routes.json
...
{
"method": "GET",
"path": "/mymodelrandom",
"handler": "mymodel.getRandom",
"config": {
"policies": []
}
},
...
4 - In your front-end, access the route
(However you access your API)
e.g. ajax call to /api/mymodelrandom
There is no API parameter for getting a random result.
So: FrontEnd is the recommended solution for your question.
You need to create a random request range and then get some random item from this range.
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
const firstID = getRandomInt(restaurants.length);
const secondID = getRandomInt(3);
const query = qs.stringify({
id_in:[firstID,secondID ]
});
// request query should be something like GET /restaurants?id_in=3&id_in=6
One way you can do this reliably is by two steps:
Get the total number of records
Fetch the number of records using _start and _limit parameters
// Untested code but you get the idea
// Returns a random number between min (inclusive) and max (exclusive)
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
const { data: totalNumberPosts } = await axios.get('/posts/count');
// Fetch 20 posts
const _limit = 20;
// We need to be sure that we are not fetching less than 20 posts
// e.g. we only have 40 posts. We generate a random number that is 30.
// then we would start on 30 and would only fetch 10 posts (because we only have 40)
const _start = getRandomArbitrary(0, totalNumberPosts - _limit);
const { data: randomPosts } = await axios.get('/posts', { params: { _limit, _start } })
The problem with this approach is that it requires two network requests but for my needs, this is not a problem.
This seem to work for me with Strapi v.4 REST API
Controller, Get 6 random entries
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries].sort(() => 0.5 - Math.random());
ctx.body = randomEntries.slice(0, numberOfEntries);
},
};
});
Route
random.js
"use strict";
module.exports = {
routes: [
{
method: "GET",
path: "/artwork/random",
handler: "artwork.random",
config: {
auth: false,
},
},
],
};
API
http://localhost:1337/api/artwork/random
To match default data structure of Strapi
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries]
.sort(() => 0.5 - Math.random())
.slice(0, numberOfEntries);
const structureRandomEntries = {
data: randomEntries.map((entry) => {
return {
id: entry.id,
attributes: entry,
};
}),
};
ctx.body = structureRandomEntries;
},
};
});
There is also a random sort plugin.
https://www.npmjs.com/package/strapi-plugin-random-sort
This seem to work for me with Strapi v4.3.8 and graphql
src/index.js
"use strict";
module.exports = {
register({ strapi }) {
const extensionService = strapi.service("plugin::graphql.extension");
const extension = ({ strapi }) => ({
typeDefs: `
type Query {
randomTestimonial: Testimonial
}
`,
resolvers: {
Query: {
randomTestimonial: async (parent, args) => {
const entries = await strapi.entityService.findMany(
"api::testimonial.testimonial"
);
const sanitizedRandomEntry =
entries[Math.floor(Math.random() * entries.length)];
return sanitizedRandomEntry;
},
},
},
resolversConfig: {
"Query.randomTestimonial": {
auth: false,
},
},
});
extensionService.use(extension);
},
bootstrap({ strapi }) {},
};
graphql query:
query GetRandomTestimonial {
randomTestimonial {
__typename
name
position
location
description
}
}
generate random testimonial on route change/refresh
https://jungspooner.com/biography
I am quite new in redux world and have not yet had a project structured the ducks way. I am trying to understand it and use it to make a mock api, since I don't have the backend ready yet. I am working with the legacy code, that I am trying to figure out. There is a folder called data, that has a duck and a backendApi file. Duck file looks like this.
data/duck.jsx
import { createSelector } from 'reselect';
import { createReduxApi } from './backendApi';
const getDataContext = state => state.default.dataContext;
const backendReduxApi = createBackendReduxApi(getDataContext);
// Action creators
export const makeRestApiRequest = endpointName => backendReduxApi .makeRequestActionCreator(endpointName);
export const resetRestApi = endpointName => backendReduxApi .makeResetActionCreator(endpointName);
// Reducers
export const dataReducer = backendReduxApi .createReducer();
// Selectors
const getRestApiState = endpointName => backendReduxApi .getEndpointState(endpointName);
export const getRestApiData = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.data);
export const getRestApiMeta = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.meta);
export const getRestApiError = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.error);
export const getRestApiStarted = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.started);
export const getRestApiFinished = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.finished);
The backendApi.jsx file looks like this:
data/backendApi.jsx
import ReduxRestApi from './rest/ReduxRestApi';
export const BackendApi = { // NOSONAR
LANGUAGE_FILE: 'languageFile',
EMPLOYEE: 'employee',
};
const backendReduxApiBuilder = ReduxRestApi.build()
/* /api */
/* /api/employee */
.withGet('/myproject/api/employee', BackendApi.EMPLOYEE)
/* /language*/
.withGet('/myproject/language/nb_NO.json', BackendApi.LANGUAGE_FILE)
export const createBackendReduxApi = restApiSelector => backendReduxApiBuilder
.withRestApiSelector(restApiSelector)
.create();
Then in the data/rest folder I have 4 files: ReduxRestApi, restConfig, RestDuck and restMethods.
data/rest/ReduxRestApi.jsx
import { combineReducers } from 'redux';
import { get, post, postAndOpenBlob } from './restMethods';
import RestDuck from './RestDuck';
class ReduxRestApi {
constructor(endpoints, getRestApiState) {
this.createReducer = this.createReducer.bind(this);
this.getEndpoint = this.getEndpoint.bind(this);
this.makeRequestActionCreator = this.makeRequestActionCreator.bind(this);
this.makeResetActionCreator = this.makeResetActionCreator.bind(this);
this.getEndpointState = this.getEndpointState.bind(this);
this.ducks = endpoints.map(({ name, path, restMethod }) => new RestDuck(name, path, restMethod, getRestApiState));
}
createReducer() {
const reducers = this.ducks
.map(duck => ({ [duck.name]: duck.reducer }))
.reduce((a, b) => ({ ...a, ...b }), {});
return combineReducers(reducers);
}
getEndpoint(endpointName) {
return this.ducks.find(duck => duck.name === endpointName)
|| { actionCreators: {} };
}
makeRequestActionCreator(endpointName) {
return this.getEndpoint(endpointName).actionCreators.execRequest;
}
makeResetActionCreator(endpointName) {
return this.getEndpoint(endpointName).actionCreators.reset;
}
getEndpointState(endpointName) {
return this.getEndpoint(endpointName).stateSelector;
}
static build() {
class RestApiBuilder {
constructor() {
this.withGet = this.withGet.bind(this);
this.withPost = this.withPost.bind(this);
this.withPostAndOpenBlob = this.withPostAndOpenBlob.bind(this);
this.withRestApiSelector = this.withRestApiSelector.bind(this);
this.endpoints = [];
}
withGet(path, name) {
this.endpoints.push({ path, name, restMethod: get });
return this;
}
withPost(path, name) {
this.endpoints.push({ path, name, restMethod: post });
return this;
}
withPostAndOpenBlob(path, name) {
this.endpoints.push({ path, name, restMethod: postAndOpenBlob });
return this;
}
withRestApiSelector(restApiSelector) {
this.restApiSelector = restApiSelector;
return this;
}
create() {
return new ReduxRestApi(
this.endpoints,
this.restApiSelector
);
}
}
return new RestApiBuilder();
}
}
export default ReduxRestApi;
restConfig.jsx
import axios from 'axios';
import { removeErrorMessage, showErrorMessage } from '../../app/duck';
import { is401Error, isHandledError } from '../../app/ErrorTypes';
const isDevelopment = process.env.NODE_ENV === 'development';
const configureRequestInterceptors = (store) => {
const onRequestAccepted = (config) => {
store.dispatch(removeErrorMessage());
return config;
};
const onRequestRejected = error => Promise.reject(error);
axios.interceptors.request.use(onRequestAccepted, onRequestRejected);
};
const configureResponseInterceptors = (store) => {
const onSuccessResponse = response => response;
const onErrorResponse = (error) => {
if (is401Error(error) && !isDevelopment) {
window.location.reload();
}
if (!isHandledError(error)) {
store.dispatch(showErrorMessage(error));
}
return Promise.reject(error);
};
axios.interceptors.response.use(onSuccessResponse, onErrorResponse);
};
const configureRestInterceptors = (store) => {
configureRequestInterceptors(store);
configureResponseInterceptors(store);
};
export default configureRestInterceptors;
data/rest/RestDuck.jsx
import { createSelector } from 'reselect';
import { get, getBlob, post, postAndOpenBlob, postBlob } from './restMethods';
/**
* getMethodName
* Helper function that maps given AJAX-method to a name
*
* Ex. getMethodName(getBlob) -> 'GET'
*/
const getMethodName = (restMethod) => {
switch (restMethod) {
case get:
case getBlob:
return 'GET';
case post:
case postBlob:
case postAndOpenBlob:
return 'POST';
default:
return '';
}
};
/**
* createRequestActionType
* Helper function to generate actionType for actions related to AJAX calls
*
* Ex: createRequestActionType('fetchEmployee', 'ERROR', get, '/myproject/api/employee') -> '##REST/fetchEmployee GET /myproject/api/employeeERROR'
*/
const createRequestActionType = (name, qualifier, restMethod = '', path = '') => [`##REST/${name}`, getMethodName(restMethod), path, qualifier]
.filter(s => s !== '')
.join(' ');
/**
* createRequestActionTypes
* Helper function to generate ActionTypes for a given AJAX method and resource.
*
* Ex. createRequestActionType(fetchEmployee, get, '/myproject/api/employee') -> {
* reset: '##REST GET /myproject/api/employee RESET',
* requestStarted: '##REST GET /myproject/api/employee STARTED',
* requestError: '##REST GET /myproject/api/employee ERROR',
* requestFinished: '##REST GET /myproject/api/employee FINISHED',
* }
*/
const createRequestActionTypes = (name, restMethod, path) => ({
reset: createRequestActionType(name, 'RESET'),
requestStarted: createRequestActionType(name, 'STARTED', restMethod, path),
requestError: createRequestActionType(name, 'ERROR', restMethod, path),
requestFinished: createRequestActionType(name, 'FINISHED', restMethod, path)
});
/**
* createRequestThunk
* Helper function that generates a thunk that performs an AJAX call specified by 'restMethod' and 'restEndpoint'
*
* When the thunk is running, the action 'requestStarted' will be dispatched immediately.
* Then, it performs the AJAX call that returns a promise.
* If the call goes well, the action 'requestFinished' will be dispatched with data from the call.
* If the call fails, the action 'requestError' is dispatched with the contents of the error.
*/
const createRequestThunk = (restMethod, restEndpoint, requestStarted, requestFinished, requestError) => (
(params, options = {}) => (dispatch) => {
dispatch(requestStarted(params, options));
return restMethod(restEndpoint, params)
.catch((error) => {
const data = error.response && error.response.data ? error.response.data : error;
dispatch(requestError(data));
return Promise.reject(error);
})
.then((response) => {
dispatch(requestFinished(response.data));
return response;
});
}
);
/**
* createRequestActionCreators
* Helper function that creates action creators 'requestStarted', 'requestFinished' and 'requestError',
* #see createRequestThunkCreator
*/
const createRequestActionCreators = (restMethod, restEndpoint, actionTypes) => {
const reset = () => ({ type: actionTypes.reset });
const requestStarted = (params, options = {}) => ({ type: actionTypes.requestStarted, payload: { params, timestamp: Date.now() }, meta: { options } });
const requestFinished = data => ({ type: actionTypes.requestFinished, payload: data });
const requestError = error => ({ type: actionTypes.requestError, payload: error });
const execRequest = createRequestThunk(restMethod, restEndpoint, requestStarted, requestFinished, requestError);
return {
reset, requestStarted, requestFinished, requestError, execRequest
};
};
/**
* createRequestReducer
*
* Helper function that creates a reducer for an AJAX call.
* Reducer alters the state of the actions with the name defined by
* actionTypes.requestStarted
* actionTypes.requestFinished
* actionTypes.requestError
*/
const createRequestReducer = (restMethod, resourceName, actionTypes) => {
const initialState = {
data: undefined,
meta: undefined,
error: undefined,
started: false,
finished: false
};
return (state = initialState, action = {}) => {
switch (action.type) {
case actionTypes.requestStarted:
return {
...initialState,
data: action.meta.options.keepData ? state.data : initialState.data,
started: true,
meta: action.payload
};
case actionTypes.requestFinished:
return {
...state,
started: false,
finished: true,
data: action.payload
};
case actionTypes.requestError:
return {
...state,
started: false,
error: action.payload
};
case actionTypes.reset:
return {
...initialState
};
default:
return state;
}
};
};
/**
* RestDuck
* Class that offers action types, action creators, reducers and selectors for an AJAX call.
* #see createRequestActionTypes
* #see createRequestActionCreators
* #see createRequestReducer
*
* Ex.
* const getEmployeeDuck = new RestDuck(execGetRequest, 'employee', GET_EMPLOYEE_SERVER_URL);
* // Action creators
* export const fetchEmployee = getEmployeeDuck.actionCreators.execRequest;
* // Reducer
* export const dataReducer = combineReducers(
* ...,
* getEmployeeDuck.reducer,
* }
* // Selectors
* export const getDataContext = state => state.default.dataContext;
* export const getEmployeeData = getEmployeeDuck.selectors.getRequestData(getDataContext);
* export const getEmployeeStarted = getEmployeeDuck.selectors.getRequestStarted(getDataContext);
* ...
*/
class RestDuck {
constructor(name, path, restMethod, getApiContext) {
this.restMethod = restMethod;
this.name = name;
this.path = path;
this.getApiContext = getApiContext;
this.$$duck = {}; // for class internal use
}
get actionTypes() {
if (!this.$$duck.actionTypes) {
this.$$duck.actionTypes = createRequestActionTypes(this.name, this.restMethod, this.path);
}
return this.$$duck.actionTypes;
}
get actionCreators() {
if (!this.$$duck.actionCreators) {
this.$$duck.actionCreators = createRequestActionCreators(this.restMethod, this.path, this.actionTypes);
}
return this.$$duck.actionCreators;
}
get reducer() {
if (!this.$$duck.reducer) {
this.$$duck.reducer = createRequestReducer(this.restMethod, this.name, this.actionTypes);
}
return this.$$duck.reducer;
}
get stateSelector() {
return createSelector([this.getApiContext], restApiContext => restApiContext[this.name]);
}
}
export default RestDuck;
data/rest/restMethods.jsx
import axios, { CancelToken } from 'axios';
const openPreview = (data) => {
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(data);
} else {
window.open(URL.createObjectURL(data));
}
};
const cancellable = (config) => {
let cancel;
const request = axios({
...config,
cancelToken: new CancelToken((c) => { cancel = c; })
});
request.cancel = cancel;
return request.catch(error => (axios.isCancel(error) ? Promise.reject(new Error(null)) : Promise.reject(error)));
};
const defaultHeaders = {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: 0
};
const defaultPostHeaders = {
'Content-Type': 'application/json'
};
export const get = (url, params, responseType = 'json') => cancellable({
url,
params,
responseType,
method: 'get',
headers: {
...defaultHeaders
}
});
export const post = (url, data, responseType = 'json') => cancellable({
url,
responseType,
data: JSON.stringify(data),
method: 'post',
headers: {
...defaultHeaders,
...defaultPostHeaders
},
cache: false
});
export const getBlob = (url, params) => get(url, params, 'blob');
export const postBlob = (url, data) => post(url, data, 'blob');
export const postAndOpenBlob = (url, data) => postBlob(url, data)
.then((response) => {
openPreview(response.data);
return {
...response,
data: 'blob opened as preview' // Don't waste memory by storing blob in state
};
});
I am not sure where to place and how to do mock api calls in this structure. I was thinking of making a mock api similiar to this one, where I would mimick the ajax calls and store them in the redux, but just not sure how to do this in this kind of setup?
I have tried with making the mockApi folder and instead of using the restMethods, to use the file where I would write promises that would resolve the mockData. This is my attempt:
mockRestMethods
const employee = {
name: 'Joe Doe'
}
const data = {
employee
};
export const get = item => new Promise((resolve) => {
setTimeout(() => {
resolve({ data: data[item] });
}, 1000);
});
But, if I inspect what is returned as the response.data inside the createRequestThunk function in the RestDuck file I get data: undefined there. Why is that, what am I doing wrong?
I may well have this wrong, but it seems like you are replacing
export const get = (url, params, responseType = 'json') => cancellable({
with export const get = item => new Promise((resolve) => { which has a different API.
Regardless, have you tried logging the value of item in the mock get function. I'm guessing it isn't "employee" which is the only property in data.
Yes, that was my goal, to replace the call that was pointing to the backend API, with the call where I would return the mock data. I have tried to log the value of the item, but I get undefined
ok, so there's an awful lot of abstraction going on there. Id suggest starting by replacing get in data/rest/restMethods.jsx directly with a version that returns a promise, get it working, and then break it out. That way you're not dealing with too many unknowns at once.
I had done similar using redux-saga. After debugging, I had found that there must be data property as root key. Here's how you should do:
const employee = {
data: { // root key of employee
items: [
{ name: 'Bhojendra' },
{ name: 'Rauniyar' }
]
}
}
// and no need to use setTimeout, we're just resolving some constant data
export const getItems = item => new Promise(resolve => resolve(employee))
Now, I hope you know why data is undefined with your code.
Still not clear?
Response looks for the data property. That's it.
I want to select all locales from my API (api/locales).
My problem is that the yield call() return the function as a plan string (yes yes plain string) and I don't have a clue why!
Maybe I missed something with yield + call + restClient response?? =\
My Saga file look like this:
import types from './types';
import actions from './actions';
import { call, put, takeEvery } from 'redux-saga/effects';
import restClient from './../../restClient';
function* getLocalesSaga() {
try {
yield put({type: types.GET_LOCALES_LOADING});
let locales;
locales = yield call(restClient, 'GET', 'locales');
console.log(locales); // this show the function string!!!
if (!locales.data) {
throw new Error('REST response must contain a data key');
}
yield put( {type: types.LOCALES_RECEIVED, locales } )
} catch (error) {
console.log(error);
yield put({type: types.GET_LOCALES_FAILURE, error})
}
}
export default function* localesSaga() {
yield [
takeEvery(types.GET_LOCALES, getLocalesSaga),
takeEvery(types.GET_LOCALES_LOADING, actions.loadingLocales),
takeEvery(types.LOCALES_RECEIVED, actions.localesReceived),
takeEvery(types.GET_LOCALES_FAILURE, actions.failedLocales),
];
}
The console.log output is:
ƒ (type, resource, params) {
if (type === __WEBPACK_IMPORTED_MODULE_0_admin_on_rest__["GET_MANY"]) {
return Promise.all(params.ids.map(function (id) {
return httpClient(apiUrl + '/' +…
There is no request in the browser networks tab.
No JS errors in console apart the console.log.
I registered the saga with customSagas={customsSagas} on the Admin component.
When I use fetch() function it works!
I want to use my restClient which include the authentication token and all logic of request and response.
The restClient is custom and this is the code:
import {
GET_LIST,
GET_ONE,
GET_MANY,
GET_MANY_REFERENCE,
CREATE,
UPDATE,
DELETE,
fetchUtils
} from 'admin-on-rest';
const { queryParameters, fetchJson } = fetchUtils;
const apiUrl = process.env.REACT_APP_API_PATH;
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const token = localStorage.getItem('token');
options.headers.set('Authorization', `Bearer ${token}`);
return fetchJson(url, options);
}
/**
* Maps admin-on-rest queries to a json-server powered REST API
*
* #see https://github.com/typicode/json-server
* #example
* GET_LIST => GET http://my.api.url/posts?_sort=title&_order=ASC&_start=0&_end=24
* GET_ONE => GET http://my.api.url/posts/123
* GET_MANY => GET http://my.api.url/posts/123, GET http://my.api.url/posts/456, GET http://my.api.url/posts/789
* UPDATE => PUT http://my.api.url/posts/123
* CREATE => POST http://my.api.url/posts/123
* DELETE => DELETE http://my.api.url/posts/123
*/
export default () => {
/**
* #param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE'
* #param {String} resource Name of the resource to fetch, e.g. 'posts'
* #param {Object} params The REST request params, depending on the type
* #returns {Object} { url, options } The HTTP request parameters
*/
const convertRESTRequestToHTTP = (type, resource, params) => {
let url = '';
const options = {};
switch (type) {
case GET_LIST: {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const query = {
...params.filter,
sort: field,
order: order,
page: page,
per_page: perPage,
};
url = `${apiUrl}/${resource}?${queryParameters(query)}`;
break;
}
case GET_ONE:
url = `${apiUrl}/${resource}/${params.id}`;
break;
case GET_MANY_REFERENCE: {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const query = {
...params.filter,
[params.target]: params.id,
_sort: field,
_order: order,
_start: (page - 1) * perPage,
_end: page * perPage,
};
url = `${apiUrl}/${resource}?${queryParameters(query)}`;
break;
}
case UPDATE:
url = `${apiUrl}/${resource}/${params.id}`;
options.method = 'PUT';
options.body = JSON.stringify(params.data);
break;
case CREATE:
url = `${apiUrl}/${resource}`;
options.method = 'POST';
options.body = JSON.stringify(params.data);
break;
case DELETE:
url = `${apiUrl}/${resource}/${params.id}`;
options.method = 'DELETE';
break;
default:
throw new Error(`Unsupported fetch action type ${type}`);
}
return { url, options };
};
/**
* #param {Object} response HTTP response from fetch()
* #param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE'
* #param {String} resource Name of the resource to fetch, e.g. 'posts'
* #param {Object} params The REST request params, depending on the type
* #returns {Object} REST response
*/
const convertHTTPResponseToREST = (response, type, resource, params) => {
const { headers, json } = response;
switch (type) {
case GET_LIST:
case GET_MANY_REFERENCE:
if (!headers.has('x-total-count')) {
throw new Error('The X-Total-Count header is missing in the HTTP Response. The jsonServer REST client expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare X-Total-Count in the Access-Control-Expose-Headers header?');
}
return {
// change the primary key to uuid
data: json.data.map(resource => resource = { ...resource, id: resource.uuid }),
total: parseInt(headers.get('x-total-count').split('/').pop(), 10),
};
case UPDATE:
case DELETE:
case GET_ONE:
return { data: json, id: json.uuid };
case CREATE:
return { data: { ...params.data, id: json.uuid } };
default:
return { data: json };
}
};
/**
* #param {string} type Request type, e.g GET_LIST
* #param {string} resource Resource name, e.g. "posts"
* #param {Object} payload Request parameters. Depends on the request type
* #returns {Promise} the Promise for a REST response
*/
return (type, resource, params) => {
if (type === GET_MANY) {
return Promise.all(params.ids.map(id => httpClient(`${apiUrl}/${resource}/${id}`)))
.then(responses => ({ data: responses.map(response => response.json) }));
}
const { url, options } = convertRESTRequestToHTTP(type, resource, params);
return httpClient(url, options)
.then(response => convertHTTPResponseToREST(response, type, resource, params));
};
};
Any one can help here and tell me why the restClient is returned as a string instead of returning the json?
It is not a GET_LIST nor GET_ONE request. It is just a normal GET request.
I tried to use GET_ONE and GET_LIST but I still get the response as the function plain string.
Edit & Solution:
Thanks to #Gildas it is clearer to use fetch instead of restClient for a GET request. restClient is used just for <resource /> and it is not so clear by the docs.
Moreover, the actions creators were useless when I use put in my main saga function.
My fetch looks like this and it works:
function getLocales() {
return fetch(process.env.REACT_APP_API_PATH + '/locales', { method: 'GET' })
.then(response => (
Promise.resolve(response)
))
.then(response => (
response.json()
))
.catch((e) => {
console.error(e);
});
}
When I called it like this:
const { languages, currentLocale } = yield call(getLocales);
Thank you in advanced.
Leo.
This part is really strange, it seems you're binding action creators as if they were sagas, though you do put them in the getLocalesSaga:
export default function* localesSaga() {
yield [
takeEvery(types.GET_LOCALES, getLocalesSaga),
takeEvery(types.GET_LOCALES_LOADING, actions.loadingLocales),
takeEvery(types.LOCALES_RECEIVED, actions.localesReceived),
takeEvery(types.GET_LOCALES_FAILURE, actions.failedLocales),
];
}
Moreover, the restClient is not fetch. GET is not a type recognized by it (see documentation). You should not use the restClient for anything that's not a resource in admin-on-rest terms. Here, you should indeed use fetch.
This should probably be rewritten like:
import types from './types';
import actions from './actions';
import { call, put, takeEvery } from 'redux-saga/effects';
import restClient from './../../restClient';
function fetchLocales() {
return fetch(...);
}
function* getLocalesSaga() {
try {
yield put(actions.loadingLocales());
let locales;
locales = yield call(fetchLocales);
console.log(locales); // this show the function string!!!
if (!locales.data) {
throw new Error('REST response must contain a data key');
}
yield put(actions.localesReceived(locales))
} catch (error) {
console.log(error);
yield put(actions.failedLocales(error))
}
}
export default function* localesSaga() {
yield takeEvery(types.GET_LOCALES, getLocalesSaga);
}
I'm trying to update the Angular2 Forms Validation example to handle an Async Validation response. This way I can hit an HTTP endpoint to validate a username.
Looking at their code they currently aren't currently using a Promise and it's working just fine:
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
const name = control.value;
const no = nameRe.test(name);
return no ? {'forbiddenName': {name}} : null;
};
}
I'm trying to update to return a Promise. Something like:
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl) => {
const name = control.value;
return new Promise( resolve => {
resolve({'forbiddenName': {name}});
});
};
}
However, the result I get doesn't display the error message, it's showing undefined.
My thought is it has something to do with the way they are handling displaying the errors:
onValueChanged(data?: any) {
if (!this.heroForm) { return; }
const form = this.heroForm;
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
However I'm not sure of a better way of doing this.
Angular2 example:
https://angular.io/docs/ts/latest/cookbook/form-validation.html#!#live-example
Link to my example attempting to return Promise:
https://plnkr.co/edit/sDs9pNQ1Bs2knp6tasgI?p=preview
The problem is that you add the AsyncValidator to the SyncValidator Array. AsyncValidators are added in a separate array after the SyncValidators:
this.heroForm = this.fb.group({
'name': [this.hero.name, [
Validators.required,
Validators.minLength(4),
Validators.maxLength(24)
],
[forbiddenNameValidator(/bob/i)] // << separate array
],
'alterEgo': [this.hero.alterEgo],
'power': [this.hero.power, Validators.required]
});
I just get to experiment with Redux and I know that middleware is essential to make ajax calls. I've installed redux-thunk and axios package separately and tried to hook my result as a state and render the ajax result to my component. However my browser console displays an error and my reducer couldn't grab the payload.
The error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
This is part of my code and how the middleware is hooked up:
//after imports
const logger = createLogger({
level: 'info',
collapsed: true,
});
const router = routerMiddleware(hashHistory);
const enhancer = compose(
applyMiddleware(thunk, router, logger),
DevTools.instrument(),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
// store config here...
my action:
import axios from 'axios';
export const SAVE_SETTINGS = 'SAVE_SETTINGS';
const url = 'https://hidden.map.geturl/?with=params';
const request = axios.get(url);
export function saveSettings(form = {inputFrom: null, inputTo: null}) {
return (dispatch) => {
dispatch(request
.then((response) => {
const alternatives = response.data.alternatives;
var routes = [];
for (const alt of alternatives) {
const routeName = alt.response.routeName;
const r = alt.response.results;
var totalTime = 0;
var totalDistance = 0;
var hasToll = false;
// I have some logic to loop through r and reduce to 3 variables
routes.push({
totalTime: totalTime / 60,
totalDistance: totalDistance / 1000,
hasToll: hasToll
});
}
dispatch({
type: SAVE_SETTINGS,
payload: { form: form, routes: routes }
});
})
);
}
}
reducer:
import { SAVE_SETTINGS } from '../actions/configure';
const initialState = { form: {configured: false, inputFrom: null, inputTo: null}, routes: [] };
export default function configure(state = initialState, action) {
switch (action.type) {
case SAVE_SETTINGS:
return state;
default:
return state;
}
}
you can see the state routes has size of 0 but the action payload has array of 3.
Really appreciate any help, thanks.
It looks like you have an unnecessary dispatch in your action, and your request doesn't look to be instantiated in the correct place. I believe your action should be:
export function saveSettings(form = { inputFrom: null, inputTo: null }) {
return (dispatch) => {
axios.get(url).then((response) => {
...
dispatch({
type: SAVE_SETTINGS,
payload: { form: form, routes: routes }
});
});
};
}