RTK Query where result is not JSON parsable - react-native

I am doing this as a learning exercise for react-toolkit:
import { createApi, fetchBaseQuery } from "#reduxjs/toolkit/query/react";
export const getIPv4AddressApi = createApi({
reducerPath: "getIPv4Address",
baseQuery: fetchBaseQuery({
baseUrl: "https://ip4.8n1.org/",
}),
endpoints: builder => ({
getIPv4Address: builder.query<string, void>({
query: () => "",
}),
}),
});
export const { useGetIPv4AddressQuery } = getIPv4AddressApi;
When run, I get an error
{"status":"PARSING_ERROR","originalStatus":200,"data":"xxx.xxx.xxx.xxx\n","error":"SyntaxError: Unexpected token . in JSON at position 6"}
How can I just return the whole response without it trying to JSON parse? Many thanks

See parsing a response
endpoints: builder => ({
getIPv4Address: builder.query<string, void>({
query: () => "",
responseHandler: "text"
}),
}),

Instead of leaving the query: as an empty string, you can specified the data format. Here is an example:enter image description here

You have to provide your own ResponseHandler in order for the response body to not be parsed as JSON. From https://redux-toolkit.js.org/rtk-query/api/fetchBaseQuery#parsing-a-response
Parsing a Response
By default, fetchBaseQuery assumes that every Response you get will be parsed as json. In the event that you don't want that to happen, you can customize the behavior by specifying an alternative response handler like text, or take complete control and use a custom function that accepts the raw Response object — allowing you to use any Response method.
The responseHandler field can be either:
TypeScript
JavaScript
type ResponseHandler =
| 'content-type'
| 'json'
| 'text'
| ((response: Response) => Promise<any>)
The "json" and "text" values instruct fetchBaseQuery to the corresponding fetch response methods for reading the body. content-type will check the header field to first determine if this appears to be JSON, and then use one of those two methods. The callback allows you to process the body yourself.
More helpful discussions here as well: https://github.com/reduxjs/redux-toolkit/issues/2440
Examples from the GitHub:
export const api = generatedAuthApi.enhanceEndpoints({
endpoints: {
users: {
query: queryArg => ({
url: '/users',
method: 'POST',
body: queryArg.body,
responseHandler: (response: { text: () => any }) => response.text()
})
},
}
})

Related

Cypress: login through magic link error with cy.origin()

Devs at my startup have switched login to a magic link system, in which you get inside after clicking a link on the email body.
I have set up a Mailsac email to receive mails containing magic links but I haven't been able to actually follow those links because of the following:
cy.request({
method: "GET",
url: "https://mailsac.com/api/addresses/xxxx#mailsac.com/messages",
headers: {
"Mailsac-Key": "here-goes-the-key",
},
}).then((res) => {
const magicLink = res.body[0].links[0];
cy.origin(magicLink, () => {
cy.visit('/')
});
});
I wasn't able to use cy.visit() either because the magic link URL is slightly different from the baseURL in this testing environment.
So my question is:
How could I follow this cumbersome link to find myself logged in home, or else, is there another way to deal with magic links?
Thanks
The docs say
A URL specifying the secondary origin in which the callback is to be executed. This should at the very least contain a hostname, and may also include the protocol, port number & path. Query params are not supported.
Not sure if this means the cy.visit() argument should not have query params, of just the cy.origin() parameter.
Try passing in the link
cy.request({
...
}).then((res) => {
const magicLink = res.body[0].links[0];
const magicOrigin = new URL(magicLink).origin
cy.origin(magicOrigin, { args: { magicLink } }, ({ magicLink }) => {
cy.visit(magicLink)
});
});
If that doesn't fix it, you could try using cy.request() but you'll have to observe where the token is stored after using the magicLink.
cy.request({
...
}).then((res) => {
const magicLink = res.body[0].links[0];
cy.request(magicLink).then(response =>
const token = response??? // find out where the auth token ends up
cy.setCookie(name, value) // for example
});
});
You need to pass the domain as the first parameter to origin, and do the visit within the callback function, something like this:
const magicLinkDomain = new Url(magicLink).hostname
cy.origin(magicLinkDomain, {args: magicLink}, ({ magicLink }) => {
cy.visit(magicLink);
//...
})
Reference: https://docs.cypress.io/api/commands/origin#Usage

Is there a way to substitute dynamic base url in RTK-Query with React Native?

I found one way. I can store base url in AsyncStorage so that the user can reload the page and still have access to that url. But there is one problem. I can’t have asynchronous code inside RTK endpoints.
const postAuthEndpoint = api.injectEndpoints({
endpoints: build => ({
postAuth: build.mutation<PostAuthResponse, PostAuthRequest>({
query: async data => {
// throws an error:
// Type 'Promise<{ url: string; method: string; body: PostAuthRequest; }>'
// is not assignable to type 'string | FetchArgs'
const baseUrl = await AsyncStorage.getItem('baseUrl');
return {
url: `${baseUrl}/auth`,
method: 'POST',
body: data,
};
},
}),
}),
});
Because of this, I decided to create a custom hook, that performs an async operation to get the base url. Then the custom hook passes the base url to the api hook from RTK-Query, which we pass to the custom hook. And returns wrapped mutation with the rest of parameters.
export const useEndpointWrapper = (endpoint: any) => {
const [mutation, ...rest] = endpoint;
const wrappedMutation = async (args: Request) => {
const baseUrl = await AsyncStorage.getItem('baseUrl');
return mutation({ baseUrl, ...args }).unwrap();
};
return [wrappedMutation, rest];
};
The main disadvantage here is that the TypeScript typing breaks down. This is solvable, but inconvenient.
Maybe there are some other ways to substitute the dynamic base url in react native?

Cypress access alias with this.* doesn't works

I'm having a little problem understanding Cypress documentation. In the alias section they've added a use case of accessing alias with fixtures using the this.* reference:
beforeEach(() => {
// alias the users fixtures
cy.fixture("users.json").as("users");
});
it("utilize users in some way", function () {
// access the users property
const user = this.users[0];
// make sure the header contains the first
// user's name
cy.get("header").should("contain", user.name);
});
But when I try to reproduce it, I keep getting the error: Cannot read property 'SOAP_body' of undefined.
I don't understand where is my error. Here is my spec:
/// <reference types="cypress"/>
describe("SOAP API Test", () => {
beforeEach(() => {
cy.fixture("SOAP_body.xml").as("SOAP_body");
});
it("Test with task", function () {
const body = this.SOAP_body;
cy.request({
method: "POST",
headers: {
"content-type": "text/xml; charset=utf-8",
Authorization: "Token myVerySecretToken",
SOAPAction: "http://tempuri.org/TrackingFull",
},
url: `https://path.of/the/application.asmx`,
body: body,
failOnStatusCode: false,
}).then((result) => {
expect(result.status).to.equal(200);
cy.task("XMLtoJSON", result.body).then((response) => {
expect(
response.elements[0].elements[1].elements[0].elements[0]
.elements[1].elements[0].elements[0].elements[0]
.elements[0].elements[0].text
).to.equal("something");
});
});
});
});
and my task
/**
* #type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
on("task", {
XMLtoJSON(XML_body) {
var convert = require("xml-js");
let result = convert.xml2js(XML_body, {
compact: false,
spaces: 2,
});
return result;
},
});
};
Using debugger just before the const definition I can see that the variables are undefined
I do know about cy.get(), but I just wanted to learn how to use the this.* pattern.
After fiddling with the code I've realized that I was using an arrow function in the step definition:
it("Test with task", () => { ... }
I've done it simply because I use a lot of code snippets in VSC, and never paid attention to the syntax is used.
So, after seeing it, I've remembered that it would never work, as the MDN documentation says:
An arrow function expression is a compact alternative to a traditional
function expression, but is limited and can't be used in all
situations.
Differences & Limitations:
Does not have its own bindings to this or super, and should not be used as methods.
Does not have arguments, or new.target keywords.
Not suitable for call, apply and bind methods, which generally rely on establishing a scope.
Can not be used as constructors.
Can not use yield, within its body.
The solution was simple as replacing it with a function definition:
it("Test with task", function () { ... }
and the this context was as expected
Moral of the history, don't trust blindly in your code editor (even if its VSC)

How do I upload files to a graphql backend implemented using graphql-upload using axios?

I have a GraphQL backend implemented using express, express-graphql, graphql and graphql-upload. My GraphQL schema declaration is as follows:
type OProject {
_id: ID!
title: String!
theme: String!
budget: Float!
documentation: String!
date: Date
}
input IProject {
title: String!
theme: String!
budget: Float!
file: Upload!
}
type Mutations {
create(data: IProject): OProject
}
type Mutation {
Project: Mutations
}
I want to make a create request to my GraphQL API at /graphql using axios. How go I go about it?
Following the GraphQL multipart request specification detailed here you would go about doing so by:
creating a FormData instance and populating it with the following:
The operations field,
the map field and,
the files to upload
Creating the FormData Instance
var formData = new FormData();
The operations field:
The value of this field will be a JSON string containing the GraphQL query and variables. You must set all file field in the variables object to null e.g:
const query = `
mutation($project: IProject!) {
Project { create(data: $project) { _id } }
}
`;
const project = {
title: document.getElementById("project-title").value,
theme: document.getElementById("project-theme").value,
budget: Number(document.getElementById("project-budget").value),
file: null
};
const operations = JSON.stringify({ query, variables: { project } });
formData.append("operations", operations);
The map field:
As its name implies, the value of this field will be a JSON string of an object whose keys are the names of the field in the FormData instance containing the files. The value of each field will be an array containing a string indicating to which field in the variables object the file, corresponding to value's key, will be bound to e.g:
const map = {
"0": ["variables.project.file"]
};
formData.append("map", JSON.stringify(map));
The files to upload
You then should add the files to the FormData instance as per the map. In this case;
const file = document.getElementById("file").files[0];
formData.append("0", file);
And that is it. You are now ready to make the request to your backend using axios and the FormData instance:
axios({
url: "/graphql",
method: "post",
data: formData
})
.then(response => { ... })
.catch(error => { ... });
To complete the answer of #Archy
If you are using definition of what you expect in GraphQl like Inputs don't forget to set your graphql mutation definition after the mutation keyword
You have to put the definition of your payload like this
const query = `
mutation MutationName($payload: Input!) {
DocumentUpload(payload: $payload) {
someDataToGet
}
}`
And your operations like this the operationName and the order doesn't matter
const operations = JSON.stringify({ operationName: "MutationName", variables: { payload: { file: null } }, query })

Storing REST response to indexedDB with Cycle.js

I'm in the middle of learninig Cycle.JS and ran into a challenge. I have a component that will get a result from an HTTP call and I'd like to persist this response in indexDB. However, I feel that the request for persistence is the responsibility of another component.
The questions I have are:
Is this a use case for a custom driver that persists HTTP responses to indexDB?
How does another component access the response stream for a request it did not make?
When I try to select the category from the HTTP source, nothing gets logged to the console. I'm using xstream, so the streams should be hot and I expect debug to output. What's going on here?
Below is my component that makes the HTTP call:
import { Feed } from './feed'
export function RssList ({HTTP, props}, feedAdapter = x => x) {
const request$ = props.url$
.map(url => ({
url: url,
method: 'GET',
category: 'rss'
}))
const response$ = HTTP
.select('rss')
.flatten()
.map(feedAdapter)
const vDom$ = response$
.map(Feed)
.startWith('')
return {
DOM: vDom$,
HTTP: request$
}
}
Here is my attempt at accessing the response at the app level:
export function main (sources) {
const urlSource = url$(sources)
const rssSink = rss$(sources, urlSource.value)
const vDom$ = xs.combine(urlSource.DOM, rssSink.DOM)
.map(([urlInput, rssList]) =>
<div>
{urlInput}
{rssList}
</div>
)
sources.HTTP.select('rss').flatten().debug() // nothing happens here
return {
DOM: vDom$,
HTTP: rssSink.HTTP
}
}
Selecting a category in the main (the parent) component is the correct approach, and is supported.
The only reason why sources.HTTP.select('rss').flatten().debug() doesn't log anything is because that's not how debug works. It doesn't "subscribe" to the stream and create side effects. debug is essentially like a map operator that uses an identity function (always takes x as input and outputs x), but with a logging operation as a side effect. So you either need to replace .debug() with .addListener({next: x => console.log(x)}) or use the stream that .debug() outputs and hook it with the operator pipeline that goes to sinks. In other words, debug is an in-between logging side effect, not a destination logging side effect.
Question #1: Custom HTTP->IDB Driver: It depends on the nature of the project, for a simple example I used a general CycleJS IDB Driver. See example below or codesandbox.io example.
Question #2: Components Sharing Streams: Since components and main share the same source/sink API you can link the output (sink) of one component to the input (source) of another. See example below or codesandbox.io example.
Question #3: debug and Logging: As the authoritative (literally) André Staltz pointed out debug needs to be inserted into a completed stream cycle, I.E., an already subscribed/listened stream.
In your example you can put debug in your RssList component:
const response$ = HTTP
.select('rss')
.flatten()
.map(feedAdapter)
.debug()
OR add a listener to your main example:
sources.HTTP.select('rss').flatten().debug()
.addListener({next: x => console.log(x)})
OR, what I like to do, is include a log driver:
run(main, {
DOM: makeDOMDriver('#app'),
HTTP: makeHTTPDriver(),
log: log$ => log$.addListener({next: log => console.log(log)}),
})
Then I'll just duplicate a stream and send it to the log sink:
const url$ = props.url
const http$ = url$.map(url => ({url: url, method: 'GET', category: 'rss'}))
const log$ = url$
return {
DOM: vdom$,
HTTP: http$,
log: log$,
}
Here's some example code for sending HTTP response to IndexedDB storage, using two components that share the data and a general IndexedDB driver:
function main(sources) {
const header$ = xs.of(div('RSS Feed:'))
const rssSink = RssList(sources) // input HTTP select and props
// output VDOM and data for IDB storage
const vDom$ = xs.combine(header$, rssSink.DOM) // build VDOM
.map(([header, rssList]) => div([header, rssList])
)
const idbSink = IdbSink(sources, rssSink.IDB) // output store and put HTTP response
return {
DOM: vDom$,
HTTP: rssSink.HTTP, // send HTTP request
IDB: idbSink.put, // send response to IDB store
log: idbSink.get, // get and log data stored in IDB
}
}
function RssList({ HTTP, props }, feedAdapter = x => x) {
const request$ = props.url$
.map(url => ({url: url, method: 'GET', category: 'rss'}))
const response$ = HTTP.select('rss').flatten().map(feedAdapter)
const idb$ = response$
const vDom$ = response$
.map(Feed)
.startWith(div('','...loading'))
return {
DOM: vDom$,
HTTP: request$,
IDB: { response: idb$ },
}
}
function Feed (feed) {
return div('> ' + feed)
}
function IdbSink(sources, idb) {
return {
get: sources.IDB.store('rss').getAll()
.map(obj => (obj['0'] && obj['0'].feed) || 'unknown'),
put: idb.response
.map(feedinfo => $put('rss', { feed: feedinfo }))
}
}
run(main, {
props: () => ({ url$: xs.of('http://lorem-rss.herokuapp.com/feed') }),
DOM: makeDOMDriver('#root'),
HTTP: makeHTTPDriver(),
IDB: makeIdbDriver('rss-db', 1, upgradeDb => {
upgradeDb.createObjectStore('rss', { keyPath: 'feed' })
}),
log: log$ => log$.addListener({next: log => console.log(log)}),
})
This is a contrived example, simply to explore the issues raised. Codesandbox.io example.