Return a Promise with Cloud Functions for Querysnapshot - angularjs

I am trying to run a querySnapshot - this function works in regular angular typescript using the toPromise operator but not in firebase cloud functions. I get an error of 'TypeError: hello.get(...).toPromise is not a function'. I know the issue is an observable versus a promise, but not sure how to create it as a promise as I can't use the rxjs operator in cloud functions.
The goal is to query the action items collection and if there is a doc that the action item name already exists in, don't create another doc with the same action item name.
index.ts file
function sleeper(userBusinessInfo: any) {
const sleep = userBusinessInfo['sleeping'];
dbFirestore
.collection('users')
.doc(`${userBusinessInfo.userID}`)
.collection('business-assessment-results')
.doc('sleep').get().then((val)=> {
const createdAt = Date.now();
actionItemsSleeping(marketing, dbFirestore, userID, createdAt)
})
}
sleep-functions.ts
export const actionItemsRest = (
data: any,
firestore: any,
UID: any,
createdAt: any,
) => {
const actionItemOrigin ="SleepingU";
const actionItemsCreatedArray = [];
if ((data?.sleeping?.name == "No or not sure") && (data?.sleeping?.name != null)) {
const actionItemName = "Get some sleep";
const actionItemResourceName = "App";
const actionItemResourceURL = "https://www.ok.com";
const actionItemResourceType = "Call";
const actionItemCategory = "Another"
const actionItemSubCategory = "Two";
const actionItemTimeFrame = "1";
const actionItemWeight =1;
createActionItem(firestore, UID, actionItemName, actionItemResourceName, actionItemResourceURL, actionItemResourceType, actionItemCategory, actionItemSubCategory, actionItemTimeFrame, actionItemOrigin, actionItemWeight, createdAt);
}
const createActionItem = (
firestore: any,
UID: any,
actionItemName: any,
actionItemResourceName: any,
actionItemResourceURL: any,
actionItemResourceType: any,
actionItemCategory: any,
actionItemSubCategory: any,
actionItemTimeFrame: any,
actionItemOrigin: any,
actionItemWeight: any,
actionItemDateCreated: any,
) => {
//run through the action items collection and if there is a doc that the action item name already exists in, don't create it again
const statusNotStarted = 'Not Started'
const ref = firestore.collection("users").doc(UID)
const hello = ref.collection('/actionItems', (ref: { where: (arg0: string, arg1: string, arg2: string) => any; }) => ref.where('actionItemName', '==', actionItemName));
return hello.get().toPromise().then((querySnapshot:any) => {
querySnapshot.forEach((doc: any) => {
doc.data();
if (querySnapshot.size > 0){
const docExists = true;
console.log(docExists);
if (!docExists){
firestore.collection("users").doc(UID).collection("actionItems").add({
actionItemName: actionItemName,
actionItemDueDate: null,
actionItemResourceName: actionItemResourceName,
actionItemResourceURL: actionItemResourceURL,
actionItemResourceType: actionItemResourceType,
actionItemCategory: actionItemCategory,
actionItemSubCategory: actionItemSubCategory,
actionItemNotes: null,
actionItemPriority: null,
actionItemDifficulty: null,
actionItemTimeFrame: actionItemTimeFrame,
actionItemStatus: statusNotStarted,
actionItemOrigin: actionItemOrigin,
actionItemWeight: actionItemWeight,
actionItemComplete: false,
actionItemDateCreated: actionItemDateCreated,
actionItemUID: UID,
});
}
}
})
})
}

Related

Why is my filter for an array returning an empty list when there should be multiple matching items

My data is stored in two separate tables; "posts" and "profiles". Each User object comes from the "profiles" table but also has a list posts which is not a column in "profiles". Because of this, I need to fetch the posts first, then their corresponding users, then add each post to their User object based on "uid". My function below works for most of that but every user has an empty posts list, even when there should be posts.
const [posts, setPosts] = useState<Array<Post>>([]);
const [profiles, setProfiles] = useState<Array<User>>([]);
useEffect(() => {
async function fetchData() {
const { data: postsData } = await supabase.from("posts").select("*");
const postUids = postsData!.map((post) => post.uid);
const { data: profilesData } = await supabase
.from("profiles")
.select("*")
.in("uid", postUids);
setPosts(postsData!.map((post) => new Post(post)));
const profiles = profilesData!.map((userData: any) => {
const userPosts: Array<Post> = posts.filter(
(post) => post.uid === userData.uid
);
console.log("User posts: " + userPosts);
const user = new User({ ...userData, posts: userPosts });
// user.posts = [...user.posts, ...userPosts];
console.log(user);
return user;
});
setProfiles((prevUsers) => [...prevUsers, ...profiles]);
console.log(profiles);
}
fetchData();
}, []);
console.log(posts);
console.log(profiles);
Example of postsData:
[{
"caption":"Caption",
"date":"1669244422569",
"imageUrls":[
"https://cdn.pixabay.com/photo/2020/05/04/16/05/mckenzie-river-5129717__480.jpg"
],
"location":{
"latitude":150,
"locationInfo":"City, State",
"longitude":-150
},
"postId":"1669244407166",
"uid":"daf6b8be-7cd0-4341-89d7-07879b207087"
}]
Post object:
export default class Post {
imageUrls: Array<string>;
postId: string;
uid: string;
caption: string;
location: Location;
date: number;
constructor(post: any) {
this.imageUrls = post.imageUrls;
this.postId = post.postId;
this.uid = post.uid;
this.caption = post.caption;
this.location = post.location;
this.date = post.date;
}
}
Example of profilesData:
{
"blockedUsers":[],
"displayName":"name",
"photoURL":"https://cdn.pixabay.com/photo/2020/05/04/16/05/mckenzie-river-5129717__480.jpg",
"uid":"daf6b8be-7cd0-4341-89d7-07879b207087",
"verified":false
}
User object:
export default class User {
uid: string;
blockedUsers: Array<string>;
posts: Array<Post>;
photoURL: string;
displayName: string;
verified: boolean;
constructor(user: any) {
this.uid = user.uid;
this.blockedUsers = user.blockedUsers;
this.posts = user.posts;
this.photoURL = user.photoURL;
this.displayName = user.displayName;
this.verified = user.verified;
}
}
Not entirely sure why you are not getting any posts data, might be due to how your RLS is configured, but there is a better way to query your data.
You can query posts and profiles at the same time like this:
const { data, error } = await supabase.from("profiles").select("*, posts(*)");
This way, you don't have to do another query to retrieve the profiles separately, and you also don't have to loop through the retrieved objects to modify them.

Sorting request response by date and then setting state with it

I currently have this function:
const fetchJobs = async (userId: string) => {
setJobs([])
await axios.post("/api/fetch/fetchMany?type=jobs", {
user_id: userId
})
.then((response) => {
if (response == null || response.data.length === 0) {
setJobs([])
} else {
const sortedJobs = response.data.sort = (a: Date, b: Date): Job[] => {
return new Date(a.startDate) - new Date(b.startDate)
}
setJobs(sortedJobs)
}
})
}
What it does is fetches a list of 'job' objects, and then attempts to sort them from newest to oldest, before placing them inside of jobs state.
However, there are two issues:
I'm getting a 'startDate' does not exist on type 'Date' in the sort function
The function is not assignable to parameter of type 'SetStateAction<Job[]>'
For some context, here's my Job type, which is an array of objects:
export type Job = {
_id: string,
user_id: string,
jobTitle: string,
employer: string,
responsibilities: string[],
startDate: string | Date,
endDate: string
}
And then here's my state type:
const [jobs, setJobs] = useState<Job[]>([])
I think I need to change what my state can be set as, but I don't understand why I can't set the state using the function, as it returns an array of job types.
Any help would be really appreciated
type the axios request await axios.post<Job[]>(...) so response.data is automatically typed accordingly.
Array#sort() is a method, not a property. It expects a callback function that recieves two values from the array (which in your case are of type Job, not Date) and returns a signed number based on their order. Not a boolean.response.data.sort((a, b) => new Date(a.startDate) - new Date(b.startDate))
And don't mix async/await with .then()
so overall
const fetchJobs = async (userId: string) => {
setJobs([])
const response = await axios.post<Job[]>("/api/fetch/fetchMany?type=jobs", {
user_id: userId
});
if (!response || response.data.length === 0) {
// setJobs([]) // you've already done that
return;
}
const sortedJobs = response.data.sort((a, b) => new Date(a.startDate) - new Date(b.startDate));
setJobs(sortedJobs);
}
Use either async/await or Promise while handling network tasks
const fetchJobs = async (userId: string) => {
setJobs([]) // 👈 Initailly set to null
const response = await axios.post<Job[]>("/api/fetch/fetchMany?type=jobs", {
user_id: userId
});
if (!response || response.data.length === 0) {
return; //👈 return here instead of using if/else
}
// 👇 Change the sort method by specifying the types
const sortedJobs = response.data.sort((a: Job, b: Job): number => new Date(a.startDate) - new Date(b.startDate));
setJobs(sortedJobs); // 👈 set the sorted Jobs here
}
For first issue:
I'm getting a 'startDate' does not exist on type 'Date' in the sort function
You can change types of sorted arguments from
const sortedJobs = response.data.sort = (a: Date, b: Date): Job[] => {
return new Date(a.startDate) - new Date(b.startDate)
}
to
const sortedJobs = response.data.sort((a: Job, b: Job): number => {
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
})
let me know if it fixed second problem, because I can't find problem with:
The function is not assignable to parameter of type 'SetStateAction<Job[]>'
but it is possible that this change fix both.

RTK Query UseLazyQuery and Typescript: how to get the correct type from hook argument

I need to create a react hook that accepts a RTK Query UseLazyQuery function (from any endpoint), apply a debounce to it and return the trigger function and results.
Everything is working as expected but I can't manage to get the correct type to the data returned by it.
As "UseLazyQuery" type requires a type for its generic type and I don't know what type it'll be, I set it to "any", but it makes the data to be typed as "any" too.
I need help to get/extract the correct type from the given "UseLazyQuery" function given to the hook and make the returned data have the correct type.
export default function useSearch<T extends UseLazyQuery<any>>(lazyQueryFn: T) {
const [ trigger, results ] = lazyQueryFn()
const getParamsAndTrigger = async (
params: ISearchParamsRequest,
filterParams?: (params: ISearchParamsRequest) => ISearchParamsRequest
) => {
await trigger(filterParams ? filterParams(params) : params, true)
}
const debouncedTrigger = useDebouncedFn(getParamsAndTrigger, 1500)
return [debouncedTrigger, { ...results }] as const
}
The "results" const returned by "lazyQueryFn" has the type "UseQueryStateDefaultResult". I need it to have the right data type.
I had a similar situation this days and I came with this approach which fixed it and should help you too:
interface ResponseData<V> {
items: V[];
cursor: string;
}
export const useGetCursorPaginatedData = <
V,
T extends UseLazyQuery<QueryDefinition<any, any, any, ResponseData<V>, any>>
>(
lazyQuery: T
) => {
const [trigger] = lazyQuery();
const { onTriggerAction, isLoading, error } = useAsyncAction();
const [items, setItems] = useState<V[]>([]);
const [newCursor, setNewCursor] = useState<string | undefined>();
const getData = async (isRefresh?: boolean) => {
const tempList = isRefresh ? [] : items;
const { data } = await trigger(isRefresh ? undefined : newCursor);
setItems([...tempList, ...data?.items]);
setNewCursor(data?.cursor);
};
const onLoadMore = useCallback(() => {
getData();
}, [newCursor]);
const onRefresh = () => {
onTriggerAction(async () => getData(true));
};
useEffect(() => {
onTriggerAction(getData);
}, []);
return {
items,
isLoading,
isError: error != null,
onLoadMore,
onRefresh
};
};
And this is how to use it:
const {
items: dailyTweets,
isLoading,
isError,
onLoadMore,
onRefresh
} = useGetCursorPaginatedData<DailyTweet, typeof useLazyGetDailyTweetsQuery>(
useLazyGetDailyTweetsQuery
);
Basically what you need extra is to define an interface for the QueryDefinition Response, in my case ResponseData and add one more generic type to function definition that is the type of the returned data in your response.
I hope this will help you.

How to get all elements from an atomFamily in recoil?

Im playing around with recoil for the first time and cant figure out how I can read all elements from an atomFamily. Let's say I have an app where a user can add meals:
export const meals = atomFamily({
key: "meals",
default: {}
});
And I can initialize a meal as follows:
const [meal, setMeal] = useRecoilState(meals("bananas"));
const bananas = setMeal({name: "bananas", price: 5});
How can I read all items which have been added to this atomFamily?
You have to track all ids of the atomFamily to get all members of the family.
Keep in mind that this is not really a list, more like a map.
Something like this should get you going.
// atomFamily
const meals = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
When creating a new objects inside the family you also have to update the mealIds atom.
I usually use a useRecoilCallback hook to sync this together
const createMeal = useRecoilCallback(({ set }) => (mealId, price) => {
set(mealIds, currVal => [...currVal, mealId]);
set(meals(mealId), {name: mealId, price});
}, []);
This way you can create a meal by calling:
createMeal("bananas", 5);
And get all ids via:
const ids = useRecoilValue(mealIds);
Instead of using useRecoilCallback you can abstract it with selectorFamily.
// atomFamily
const mealsAtom = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set}, meal) => {
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
Further more, in case you would like to support reset you can use the following code:
// atomFamily
const mealsAtom = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set, reset}, meal) => {
if(meal instanceof DefaultValue) {
// DefaultValue means reset context
reset(mealsAtom(id));
reset(mealIds (id));
return;
}
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
If you're using Typescript you can make it more elegant by using the following guard.
import { DefaultValue } from 'recoil';
export const guardRecoilDefaultValue = (
candidate: unknown
): candidate is DefaultValue => {
if (candidate instanceof DefaultValue) return true;
return false;
};
Using this guard with Typescript will look something like:
// atomFamily
const mealsAtom = atomFamily<IMeal, number>({
key: "meals",
default: {}
});
const mealIds = atom<number[]>({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily<IMeal, number>({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set, reset}, meal) => {
if (guardRecoilDefaultValue(meal)) {
// DefaultValue means reset context
reset(mealsAtom(id));
reset(mealIds (id));
return;
}
// from this line you got IMeal (not IMeal | DefaultValue)
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
You can use an atom to track the ids of each atom in the atomFamily. Then use a selectorFamily or a custom function to update the atom with the list of ids when a new atom is added or deleted from the atomFamily. Then, the atom with the list of ids can be used to extract each of the atoms by their id from the selectorFamily.
// File for managing state
//Atom Family
export const mealsAtom = atomFamily({
key: "meals",
default: {},
});
//Atom ids list
export const mealsIds = atom({
key: "mealsIds",
default: [],
});
This is how the selectorFamily looks like:
// File for managing state
export const mealsSelector = selectorFamily({
key: "mealsSelector",
get: (mealId) => ({get}) => {
return get(meals(mealId));
},
set: (mealId) => ({set, reset}, newMeal) => {
// if 'newMeal' is an instance of Default value,
// the 'set' method will delete the atom from the atomFamily.
if (newMeal instanceof DefaultValue) {
// reset method deletes the atom from atomFamily. Then update ids list.
reset(mealsAtom(mealId));
set(mealsIds, (prevValue) => prevValue.filter((id) => id !== mealId));
} else {
// creates the atom and update the ids list
set(mealsAtom(mealId), newMeal);
set(mealsIds, (prev) => [...prev, mealId]);
}
},
});
Now, how do you use all this?
Create a meal:
In this case i'm using current timestamp as the atom id with Math.random()
// Component to consume state
import {mealsSelector} from "your/path";
import {useSetRecoilState} from "recoil";
const setMeal = useSetRecoilState(mealsSelector(Math.random()));
setMeal({
name: "banana",
price: 5,
});
Delete a meal:
// Component to consume state
import {mealsSelector} from "your/path";
import {DefaultValue, useSetRecoilState} from "recoil";
const setMeal = useSetRecoilState(mealsSelector(mealId));
setMeal(new DefaultValue());
Get all atoms from atomFamily:
Loop the list of ids and render Meals components that receive the id as props and use it to get the state for each atom.
// Component to consume state, parent of Meals component
import {mealsIds} from "your/path";
import {useRecoilValue} from "recoil";
const mealIdsList = useRecoilValue(mealsIds);
//Inside the return function:
return(
{mealIdsList.slice()
.map((mealId) => (
<MealComponent
key={mealId}
id={mealId}
/>
))}
);
// Meal component to consume state
import {mealsSelector} from "your/path";
import {useRecoilValue} from "recoil";
const meal = useRecoilValue(mealsSelector(props.id));
Then, you have a list of components for Meals, each with their own state from the atomFamily.
Here is how I have it working on my current project:
(For context this is a dynamic form created from an array of field option objects. The form values are submitted via a graphql mutation so we only want the minimal set of changes made. The form is therefore built up as the user edits fields)
import { atom, atomFamily, DefaultValue, selectorFamily } from 'recoil';
type PossibleFormValue = string | null | undefined;
export const fieldStateAtom = atomFamily<PossibleFormValue, string>({
key: 'fieldState',
default: undefined,
});
export const fieldIdsAtom = atom<string[]>({
key: 'fieldIds',
default: [],
});
export const fieldStateSelector = selectorFamily<PossibleFormValue, string>({
key: 'fieldStateSelector',
get: (fieldId) => ({ get }) => get(fieldStateAtom(fieldId)),
set: (fieldId) => ({ set, get }, fieldValue) => {
set(fieldStateAtom(fieldId), fieldValue);
const fieldIds = get(fieldIdsAtom);
if (!fieldIds.includes(fieldId)) {
set(fieldIdsAtom, (prev) => [...prev, fieldId]);
}
},
});
export const formStateSelector = selectorFamily<
Record<string, PossibleFormValue>,
string[]
>({
key: 'formStateSelector',
get: (fieldIds) => ({ get }) => {
return fieldIds.reduce<Record<string, PossibleFormValue>>(
(result, fieldId) => {
const fieldValue = get(fieldStateAtom(fieldId));
return {
...result,
[fieldId]: fieldValue,
};
},
{},
);
},
set: (fieldIds) => ({ get, set, reset }, newValue) => {
if (newValue instanceof DefaultValue) {
reset(fieldIdsAtom);
const fieldIds = get(fieldIdsAtom);
fieldIds.forEach((fieldId) => reset(fieldStateAtom(fieldId)));
} else {
set(fieldIdsAtom, Object.keys(newValue));
fieldIds.forEach((fieldId) => {
set(fieldStateAtom(fieldId), newValue[fieldId]);
});
}
},
});
The atoms are selectors are used in 3 places in the app:
In the field component:
...
const localValue = useRecoilValue(fieldStateAtom(fieldId));
const setFieldValue = useSetRecoilState(fieldStateSelector(fieldId));
...
In the save-handling component (although this could be simpler in a form with an explicit submit button):
...
const fieldIds = useRecoilValue(fieldIdsAtom);
const formState = useRecoilValue(formStateSelector(fieldIds));
...
And in another component that handles form actions, including form reset:
...
const resetFormState = useResetRecoilState(formStateSelector([]));
...
const handleDiscard = React.useCallback(() => {
...
resetFormState();
...
}, [..., resetFormState]);

How to return single type from function

I'm trying to to build a helper function in Typescript (React). I defined a function that return a object or object[] based on the response data.
Now when i use the function the return type is T | T[] and this needs to be T or T[] based on the data.
My helper function
import { useQuery } from '#apollo/react-hooks';
import { flatMap } from 'lodash';
export default function QueryHelper<T> (document: any, variables?: {}) {
const { data: responseData, loading, error } = useQuery(document, { variables });
const object = (): T => responseData;
const array = (): T[] => flatMap(responseData);
let data;
if (flatMap(responseData).length === 1) {
data = object();
} else {
data = array();
}
return { data, loading, error };
}
Call to the function
const objects = QueryHelper<Object>(multipleObjectsDocument);
const object = QueryHelper<Object>(singleObjectDocument, { id });
return types
const Object: {
data: Object| Object[]; // This needs to be 1 type
loading: boolean;
error: ApolloError | undefined;
}
The main idea is that i can call for example;
const name = object.data.name';
const listOfName = objects.data.map(obj => obj.name);
now i get the following error
Property 'map' does not exist on type 'Object | Object[]'.
i also tried to conditionally return different variables bases on a if statement but this returns;
const Object: {
object: Object;
loading: boolean;
error: ApolloError | undefined;
} | {
array: Object[];
loading: boolean;
error: ApolloError | undefined;
}
Just fixed it.
export default function QueryHelper<T> (document: any, variables?: {}): queryResponse<T> {
const { data: responseData, loading, error } = useQuery(document, { variables });
let data = responseData ?? [];
const array = flatMap(data);
if (!loading && array.length === 1) {
data = array[0];
} else {
data = array;
}
return { data, loading, error };
}
calls
const objects = QueryHelper<Object[]>(MultipleObjectsDocument).data; // returns type Object[]
const object = QueryHelper<Object>(ObjectDocument, { id }).data; // returns type Object

Resources