I'm new to Typescript and have been doing a refactor a colleague code, I'm currently doing a typecheck and removing all any types. The goal is to make an MSGraph API call and return the a JSON file that translated into BirthdayPerson with a name, birthday date and a ID
I've been trying to a assign a type instead of any in the following code, but whether I assign number, string or any other type a different error will show up.
Perhaps I'm not tackling the solution correctly:
graph.ts
* #param accessToken
* #param endpoint url to call from MS Graph
*/
async function callMsGraph(accessToken: string, endpoint: string) {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append('Authorization', bearer);
const options = {
method: 'GET',
headers,
};
try {
return fetch(endpoint, options);
} catch (error) {
console.error(error);
throw error;
}
}
export const callMsGraphWithoutPagination = async (
accessToken: string,
url: string,
dataToReturn: any[] = []
): Promise<any[]> => {
try {
const data = await callMsGraph(accessToken, url);
const dataJson = await data.json();
const newData = dataToReturn.concat(dataJson.value);
if (dataJson['#odata.nextLink']) {
const NEXT_URL = dataJson['#odata.nextLink'].split('/v1.0')[1];
return await callMsGraphWithoutPagination(
accessToken,
process.env.REACT_APP_GRAPH_URL + NEXT_URL,
newData
);
}
return dataToReturn.concat(dataJson.value);
} catch (error) {
/* eslint-disable no-console */
console.error(error);
/* eslint-enable no-console */
throw error;
}
};
export default callMsGraph;
useUsers.tsx
export const useUsers = () => {
const token = useToken();
const [users, setUsers] = React.useState<BirthdayPerson[]>([]);
React.useEffect(() => {
if (token) {
callMsGraphWithoutPagination(token, graphConfig.usersEndpoint).then(async (data: any) => {
const processedData: any[] = await Promise.all(
data.map(async (element: any) => {
const user = await callMsGraph(token, graphConfig.userBirthdayEndpoint(element.id));
const userJson = await user.json();
const image = await callMsGraph(token, graphConfig.userPhotoEndpoint(element.id));
const blob = await image.blob();
const returnElement: BirthdayPerson = {
displayName: element.displayName,
birthday: userJson.value,
id: element.id,
};
if (blob !== null) {
window.URL = window.URL || window.webkitURL;
returnElement.picture = window.URL.createObjectURL(blob);
}
return returnElement;
})
);
setUsers([].concat(...processedData));
});
}
}, [token]);
return users;
};
helpers.ts
interface IUpdateData {
slideCount: number;
}
const sortAndFilterBirthdays = (people: BirthdayPerson[], daysToGet: number) =>
people
.sort((firstEl, secondEl) => sortDate(firstEl.birthday, secondEl.birthday))
.filter(({ birthday }) => filterByAmountOfDays({ date: birthday, daysAfter: daysToGet }));
const getBirthdays: any = (people: BirthdayPerson[], daysToGet: number) => {
const validBirthdays = people.filter((element: any) => {
const year = moment(element.birthday).year();
return year !== 0;
});
const result = sortAndFilterBirthdays(validBirthdays, daysToGet);
// if it's okay
if (result.length > 1 && daysToGet <= 30) {
return result;
}
// if not okay, filters by future dates, concats with 'next year' dates, returns 2 dates
const fallbackResult = validBirthdays
.sort((firstEl, secondEl) => sortDate(firstEl.birthday, secondEl.birthday))
.filter((person: BirthdayPerson) => {
const currentYear = moment().year();
const date = moment(person.birthday, DATE_FORMAT).set('years', currentYear);
return moment().diff(date, 'days') <= 0;
});
return fallbackResult.concat(validBirthdays).splice(0, 2);
};
Any help or indication would be great!
From all the changes I've done another object will complain that Type 'x' is not assignable to type 'string'
Firstly you need to somehow type responses from API, because right now, as you have seen, call to .json() on Response object returns unknown, which make sense because no one knows what response the server returns. You may know what response it is expected to return, but not what it actually does.
Ideally therefore you need a parser that will verify that the response has correct structure and throws an error otherwise. There are libraries such as superstruct, yup, joi and others which you can use for this. Of course this is a lot of work and will need refactoring. Or if you don't care enough you can just cast the response object to appropriate type, but then if server returns something unexpected and the application cannot handle it, it's your fault.
Example with response parsing using superstruct
import {string, number, create, Infer} from 'superstruct'
// I assume `BirthdayUser` is just a string, but you can type more complex objects as well
const BirthdayUser = string()
// This is so that you don't have to list fields twice: once
// for the parser and once for typescript
type BirthdayUser = Infer<typeof BirthdayUser>
// Then use the parser
const response = await callMsGraph(acessToken, url)
const userJson = await response.json()
// user variable has inferred appropriate type, and if the response doesn't
// comply with the definition of `BirthdayUser` an error will be thrown
// Also I assume MS graph doesn't just return plain value but wraps it in an object with `value` field, so writing it here
const user = create(userJson, object({ value: BirthdayUser }))
Example of "I don't care enough" solution
type BirthdayUser = string
const response = await callMsGraph(accessToken, url)
// Same thing with wrapping into object with `value` field
const userJson = (await response.json()) as {value: BirthdayUser}
This is a bit awkward, because API call returns Response object and not the actual data. It might be easier to work with if you move parsing and casting logic inside of callMsGraph. It's not obligatory of course, but I still provide an example because after that you need to type callMsGraphWithoutPagination and it will use the same idea
import {object, create, Struct} from 'superstruct'
async function callMsGraphParsed<T>(
accessToken: string,
url: string,
// Since we need information about object structure at runtime, just making function
// generic is not enough, you need to pass the parser structure as an argument
struct: Struct<T>
) {
// ...
const response = await fetch(...)
const json = await response.json()
// Same, verifies that response complies to provided structure, if it
// does returns type object (of type `T`), otherwise throws an error
return create(json, object({ value: struct }))
}
async function callMsGraphLazy<T>(accessToken: string, url: string) {
// ...
const response = await fetch(...)
const json = await response.json()
return json as {value: T}
}
However I only call .json() here, if you want to use this solution, you will then need either a different function or another argument if you also want it to call .blob() for some API calls.
Now you type callMsGraphWithoutPagination using in the same way:
export const callMsGraphWithoutPaginationParsed = async <T>(
accessToken: string,
url: string,
dataToReturn: T[] = [],
struct: Struct<T>,
): Promise<T[]> => {
// If you rewrote `callMsGraph` to use parsing
const dataJson = await callMsGraph(accessToken, url, struct);
const newData = dataToReturn.concat(dataJson.value);
// ...
}
export const callMsGraphWithoutPaginationLazy= async <T>(
accessToken: string,
url: string,
dataToReturn: T[] = [],
): Promise<T[]> => {
// If you left `callMsGraph` as is
const data = await callMsGraph(accessToken, url);
const dataJson = (await data.json()) as {value: T}
const newData = dataToReturn.concat(dataJson.value);
// ...
}
Then use it
// Not sure if you are requesting `BirthdayUser` array here or some other entity, so change it to whatever you expect to receive
callMsGraphWithoutPagination<BirthdayUser>(token, graphConfig.usersEndpoint).then(async (data) => {
// "data" is inferred to have type BirthdayUser[]
data.map(element => {
// "element" is inferred to have type BirthdayUser
})
})
Also everywhere I wrote "I assume" and "Not sure" is missing info that you should probably have provided in the question. It didn't turn out to be relevant for me, but it could have. Good luck!
Related
I'm trying to update some metadata for 10 JSON files, some of them get updated properly and some others give me the following error.
Below you can see 6 files give me errors and 4 files are correctly updated.
error:TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
error:TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
error:TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
error:TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
error:TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
error:TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
5.png uploaded & 5.json updated!
4.png uploaded & 4.json updated!
1.png uploaded & 1.json updated!
7.png uploaded & 7.json updated!
This is the node.js code im using:
onst FormData = require('form-data');
const fetch = require('node-fetch');
const path = require("path");
const basePath = process.cwd();
const fs = require("fs");
fs.readdirSync(`${basePath}/build/images`).
forEach(file => {
const formData = new FormData();
const fileStream = fs.createReadStream(`${basePath}/build/images/${file}`);
formData.append("file", fileStream);
let url = 'https://api.nftport.xyz/v0/files';
let options = {
method: 'POST',
headers: {
Authorization: 'SecretApiCode',
},
body: formData
};
fetch(url, options)
.then(res => res.json())
.then((json) => {
const fileName = path.parse(json.file_name).name;
let rawdata = fs.readFileSync(`${basePath}/build/json/${fileName}.json`);
let metaData = JSON.parse(rawdata);
metaData.file_url = json.ipfs_url;
fs.writeFileSync(`${basePath}/build/json/${fileName}.json`,
JSON.stringify(metaData, null, 2));
console.log(`${json.file_name} uploaded & ${fileName}.json updated!`);
})
.catch(err => console.error('error:' + err));
});
I have 10png files:
1.png
2.png
3.png
4.png
5.png
6.png
7.png
8.png
9.png
10.png
And 10 JSON files
1.json
2.json
3.json
4.json
5.json
6.json
7.json
8.json
9.json
10.json
The issue in your code seems to be with fetching the URL.
This is proven as sometimes, your code fails with an error, and sometimes, it works successfully.
What might be happening is that sometimes, when you are fetching the data from the URL, it might not have the file_name property (because of an error, etc.)
When you are trying to parse the path with the below method:
path.parse(json.file_name);
The file_name might be undefined as there might not be such a key. And, as path.parse() can't parse undefined, it returns an error, saying that in only accepts strings (shown below).
TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
There are many possible solutions, so they are listed below.
Add a check for if file_name is available. If it isn't, then don't try parsing it with path.parse(). You can use the code below to check it (in the second .then() method).
if (!json.file_name) {
console.error("Error fetching file name.");
return;
}
If your API supports it, check if there is an error in the response body. Assuming that the error will be something like .error, you can use the code below for guidance (add it in the second .then() method).
if (!json.file_name && json.error) {
console.log(json.error.message);
return;
}
There may be many other (better) solutions then these, but these solutions are just to give you a starting point.
Solution 1 and 2 are very similar, but they work under different circumstances.
Try this. You can also find it in codeSTACKr/create-10k-nft-collection" git repository if you change tag to v1.2.0.
This is in uploadfiles.js
const FormData = require("form-data");
const fetch = require("node-fetch");
const path = require("path");
const basePath = process.cwd();
const fs = require("fs");
const AUTH = 'Your API KEY GOES HERE';
const TIMEOUT = 1000; // Milliseconds. Extend this if needed to wait for each upload. 1000 = 1 second.
const allMetadata = [];
async function main() {
const files = fs.readdirSync(`${basePath}/build/images`);
files.sort(function(a, b){
return a.split(".")[0] - b.split(".")[0];
});
for (const file of files) {
const fileName = path.parse(file).name;
let jsonFile = fs.readFileSync(`${basePath}/build/json/${fileName}.json`);
let metaData = JSON.parse(jsonFile);
if(!metaData.file_url.includes('https://')) {
const response = await fetchWithRetry(file);
metaData.file_url = response.ipfs_url;
fs.writeFileSync(
`${basePath}/build/json/${fileName}.json`,
JSON.stringify(metaData, null, 2)
);
console.log(`${response.file_name} uploaded & ${fileName}.json updated!`);
} else {
console.log(`${fileName} already uploaded.`);
}
allMetadata.push(metaData);
}
fs.writeFileSync(
`${basePath}/build/json/_metadata.json`,
JSON.stringify(allMetadata, null, 2)
);
}
main();
function timer(ms) {
return new Promise(res => setTimeout(res, ms));
}
async function fetchWithRetry(file) {
await timer(TIMEOUT)
return new Promise((resolve, reject) => {
const fetch_retry = (_file) => {
const formData = new FormData();
const fileStream = fs.createReadStream(`${basePath}/build/images/${_file}`);
formData.append("file", fileStream);
let url = "https://api.nftport.xyz/v0/files";
let options = {
method: "POST",
headers: {
Authorization: AUTH,
},
body: formData,
};
return fetch(url, options).then(async (res) => {
const status = res.status;
if(status === 200) {
return res.json();
}
else {
console.error(`ERROR STATUS: ${status}`)
console.log('Retrying')
await timer(TIMEOUT)
fetch_retry(_file)
}
})
.then(async (json) => {
if(json.response === "OK"){
return resolve(json);
} else {
console.error(`NOK: ${json.error}`)
console.log('Retrying')
await timer(TIMEOUT)
fetch_retry(_file)
}
})
.catch(async (error) => {
console.error(`CATCH ERROR: ${error}`)
console.log('Retrying')
await timer(TIMEOUT)
fetch_retry(_file)
});
}
return fetch_retry(file);
});
}
I tried to add a call to an endpoint in order to get translation. I have like this :
const loadLocales = async () => {
const context = require.context('./locales', true);
const data = await ApiService.post(`${translationToolUrl}/gateway/translations`, { project: 'myProject' });
const messages = context.keys()
.map((key) => ({ key, locale: key.match(/[-a-z0-9_]+/i)[0] }))
.reduce((msgs, { key, locale }) => ({
...msgs,
[locale]: extendMessages(context(key)),
}), {});
return { context, messages };
};
const { context, messages } = loadLocales();
i18n = new VueI18n({
locale: 'en',
fallbackLocale: 'en',
silentFallbackWarn: true,
messages,
});
if (module.hot) {
module.hot.accept(context.id, () => {
const { messages: newMessages } = loadLocales();
Object.keys(newMessages)
.filter((locale) => messages[locale] !== extendMessages(newMessages[locale]))
.forEach((locale) => {
const msgs = extendMessages(newMessages[locale]);
messages[locale] = msgs;
i18n.setLocaleMessage(locale, msgs);
});
});
}
I added this request : ApiService.post. But I have the error TypeError: context is undefined droped at this line module.hot.accept(context.id.... Have you an idea how I can solve that ? My scope was to add this request in order to get translations from database and from .json files for now. I want to do a merge between both for now, in the feature I will get only from database but this will be done step by step.
The problem is, that you trying to declare multiple const in the wrong way, independently of trying to declaring them twice. This shows in:
const { context, messages } = loadLocales();
This would couse context and messages to be undefined. This won´t give an error, as I replicated in a small example:
const {first, second} = 'Testing'
console.log(first)
console.log(second)
Both, first and second, will be undefined. If you try to declare multiple const at once, you need to do it this way:
const context = loadLocales(), messages = loadLocales();
I'm trying to do a generator function with yield inside a Redux saga.
I'm trying to get a value from my endpoint ind function g1 by calling function g2. Don't know id this is the right way to do it. I only want to get data from my endpoint if isTranslatable is true.
But fore some reason I get a response undefined. What am I missing. And yes I'm new at this.
service file
export const fetchEnglishFilterValue = async (
countrySelectorCountry: string,
selectedFilter: string,
selectedFilterValue: string,
): Promise<Response> => {
const url = `${DONORS_SERVICE_URL}/api/donors/translate/${countrySelectorCountry}/${selectedFilter}/${selectedFilterValue}`;
return get(url);
};
saga file
export function* g1() {
const englishFilterValue = g2(selectedFilter.key, selectedFilter.value);
console.log(isTranslatable ? englishFilterValue : "normal value");
console.log(englishFilterValue.next().value);
}
export function* g2(filterKey: string, filterValue: string) {
try {
const response: Response = yield call(fetchEnglishFilterValue, appSettings.language, filterKey, filterValue);
console.log("response: ", response);
const data: string = yield call([response, response.json]);
console.log("data: ", data);
return data;
} catch {
error(`Getting english filter value for datalayer. countrySelectorCountry: ${appSettings.language}, selectedFilter: ${filterKey}, selectedFilterValue: ${filterValue}`);
return undefined;
}
}
Using Next API routes and Knex + MySQL, and using React and SWR for fetching, I get a strange bug. If a request fails, my queries start appending , * to the select statement, resulting in SQL syntax errors. For example, the query should use select *, but comes out as select *, * and then select *, *, * and so on. Does anyone have any idea why this might happen?
SWR fetch:
export const swrFetcher = async (...args) => {
const [url, contentType = 'application/json'] = args;
const res = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': contentType,
},
});
if (!res.ok) throw new Error(res.statusText);
const json = await res.json();
return json;
};
const { data, error } = useSWR('/api/user/me', swrFetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateOnMount: true,
});
knex query:
const User = knex(TABLE_NAMES.user);
export const readById = (id) => User.select('*').where({ id });
You probably need to create the knex instance within the function call and not reuse the same every time, like it's currently happening.
export const readById = (id) => {
const User = knex(TABLE_NAMES.user);
return User.select('*').where({ id });
}
Say I have a Request object:
{
user: { /* user data: username, email, etc. */ }
post: { /* post data: content, date, etc. */ }
}
Example of a Request object:
{
user: {
id: '123'
username: 'kibe'
email: 'blabla#gmail.com'
}
post: {
content: 'my new post!'
date: '20/02/2004'
}
}
Now, I have two functions: validateUser and validatePost. Both of them return a Maybe monad, because they might fail.
How can I then do something like this?
function savePost (request) {
return request
|> validateUser // if validateUser returns either Success(user) or Failure(error), how would I pass down the post?
|> validatePost
|> savePostToDb
|> ok
}
Should I create a function validateRequest which composes validateUser and validatePost? But then, how would I only give the post object to savePostToDb? What if savePostToDb also requires the user ID?
function savePost (request) {
return request
|> validateRequest // returns an Either monad
|> savePostToDb // only requires user ID and the post, how would I pass these down?
|> ok
}
Hopefully these questions make sense. I am new to FP and although I am understanding its paradigms, I am failing to design a simple program
Thanks!
prop
We write a simple prop function which safely looks up properties on your object. If they are present, we get a Just-wrapped value, otherwise we get Nothing -
const fromNullable = x =>
x == null
? Nothing
: Just(x)
const prop = k => t =>
fromNullable(t[k])
props
You have nested properties though, so let's make it so we can dig arbitrarily deep into your object using any sequence of props -
const props = (k, ...ks) => t =>
ks.length
? prop(k)(t).bind(props(...ks))
: prop(k)(t)
validate
Now we can write a simple validate coroutine -
function* validate (t)
{ const id = yield props("user", "id")(t)
const username = yield props("user", "username")(t)
const email = yield props("user", "email")(t)
const post = yield props("post", "content")(t)
const date = yield props("post", "date")(t)
return Just({ id, username, email, post, date }) // <- Just(whateverYouWant)
}
const doc =
{ user:
{ id: '123'
, username: 'kibe'
, email: 'blabla#gmail.com'
}
, post:
{ content: 'my new post!'
, date: '20/02/2004'
}
}
coroutine(validate(doc)).bind(console.log)
With a valid doc we see -
{
id: '123',
username: 'kibe',
email: 'blabla#gmail.com',
post: 'my new post!',
date: '20/02/2004'
}
With an invalid document, otherdoc -
const otherdoc =
{ user:
{ id: '123'
, username: 'kibe'
, // <- missing email
}
, post:
{ content: 'my new post!'
, // <- missing date
}
}
coroutine(validate(otherdoc)).bind(console.log)
// no effect because validate returns a Nothing!
coroutine
Implementation of coroutine is simple and most importantly it is generic for any monad -
function coroutine (f)
{ function next (v)
{ let {done, value} = f.next(v)
return done ? value : value.bind(next)
}
return next()
}
Maybe
Lastly we supply implementation for Nothing and Just -
const Nothing =
({ bind: _ => Nothing })
const Just = v =>
({ bind: f => f(v) })
demo
Expand the snippet below to verify the results in your own browser -
const Nothing =
({ bind: _ => Nothing })
const Just = v =>
({ bind: f => f(v) })
const fromNullable = x =>
x == null
? Nothing
: Just(x)
const prop = k => t =>
fromNullable(t[k])
const props = (k, ...ks) => t =>
ks.length
? prop(k)(t).bind(props(...ks))
: prop(k)(t)
function coroutine (f)
{ function next (v)
{ let {done, value} = f.next(v)
return done ? value : value.bind(next)
}
return next()
}
function* validate (t)
{ const id = yield props("user", "id")(t)
const username = yield props("user", "username")(t)
const email = yield props("user", "email")(t)
const post = yield props("post", "content")(t)
const date = yield props("post", "date")(t)
return Just({ id, username, email, post, date })
}
const doc =
{user:{id:'123',username:'kibe',email:'blabla#gmail.com'},post:{content:'my new post!',date:'20/02/2004'}}
coroutine(validate(doc)).bind(console.log)
related reading
How do pipes and monads work together in JavaScript?
Ramda: Fold an object
How should i sort a list inside an object in functional programming with different logic based on different condition?
Sort objects in array with dynamic nested property keys
Yes you should create validateRequest and a savePostToDb Methodes which give you a boolean back. Then you can simple create two Maybe-Functions with thos two new methodes and compose it togheter. Look at my example how I would do it simple:
// Here some simple Left/Right to Just and Nothing (Maybe)-Monade function to work with
const Left = x => f => _ => f(x);
const Right = x => _ => g => g(x);
const Nothing = Left();
const Just = Right;
// your maybe-validateRequest function
const maybeValidateRequest = request =>
validateRequest(request)
? Just(user)
: Nothing
// your maybe-postToDb function
const maybePostToDb = request =>
savePostToDb(request)
? Just(request)
: Nothing
// and finally compose those two maybes and do the postRequest finally
const savePost = request =>
maybeValidateRequest(request)
(() => console.error("Validation failed"))
(_ =>
maybePostToDb(request)
(() => console.error("Save to DB failed")))
(_ => postRequest(request)) // when it's everything passed,the postRequest can be safely made