Custom resolve function returned undefined in grandstack - javascript

I have type definitions for A and B defined in the schema.graphql file. Resolvers for these are auto-generated and work well (in other places).
To create a scoring mechanism that ranks nodes with label B in relation to the node with label A, I am writing a CustomResolver query that executes a cypher query and returns a collection of bs and a computed score as defined in the ScoredBs type.
The schema.graphql file looks like this.
type Query {
CustomResolver(idA: ID!, idsB: [ID!]!): [ScoredBs]
}
type A {
id: ID! #id
# and some more stuff of course
}
type B {
id: ID! #id
# more fields that use the #relation or #cypher decorator
}
type ScoredBs {
bs: [B]
score: Float
}
This is where the custom resolver is defined:
const resolvers = {
Query: {
CustomResolver: async (
parent,
{ idA, idsB },
context,
info
) => {
const cypher = `
MATCH (a:A {id: $idA})
MATCH (a)<--(b:B) WHERE b.id IN $idsB
WITH
b
RETURN DISTINCT collect(b) AS bs, rand() AS score
ORDER BY score DESC
`
const session = context.driver.session()
const results = await session
.run(cypher, { idA, idsB })
.then((result) => {
return result.records.map((record) => {
return {
bs: record.get('bs'),
score: record.get('score')?.low || null,
}
})
})
.catch(console.log)
.then((results) => {
session.close()
return results
})
return results
},
},
}
When I run the query in the apollo-server graphql playground i am receiving an error:
"message": "Resolve function for "B.id" returned undefined",
query {
CustomResolver(
idA:"2560886f-654b-4047-8d7a-386cd7d9f670",
idsB: ["01ec8367-a8ae-4600-9f88-ec141b2cae2c", "032f9a88-98c3-4968-8388-659ae26d63b3"]
) {
bs {
id
}
score
}
}

I solved this by rewriting part of the code:
const results = await session
.run(cypher, { idA, idsB })
.then((result) => {
return result.records.map((record) => {
// diff start
const obj = record.toObject()
const bs = obj.bs.map((b) => b.properties)
return {
bs, // diff end
score: record.get('score')?.low || null,
}
})
})
.catch(console.log)
.then((results) => {
session.close()
console.log(results)
return results
})
return results

Related

How to update RTK Query cache when Firebase RTDB change event fired (update, write, create, delete)

I am using redux-tookit, rtk-query (for querying other api's and not just Firebase) and Firebase (for authentication and db).
The code below works just fine for retrieving and caching the data but I wish to take advantage of both rtk-query caching as well as Firebase event subscribing, so that when ever a change is made in the DB (from any source even directly in firebase console) the cache is updated.
I have tried both updateQueryCache and invalidateTags but so far I am not able to find an ideal approach that works.
Any assistance in pointing me in the right direction would be greatly appreciated.
// firebase.ts
export const onRead = (
collection: string,
callback: (snapshort: DataSnapshot) => void,
options: ListenOptions = { onlyOnce: false }
) => onValue(ref(db, collection), callback, options);
export async function getCollection<T>(
collection: string,
onlyOnce: boolean = false
): Promise<T> {
let timeout: NodeJS.Timeout;
return new Promise<T>((resolve, reject) => {
timeout = setTimeout(() => reject('Request timed out!'), ASYNC_TIMEOUT);
onRead(collection, (snapshot) => resolve(snapshot.val()), { onlyOnce });
}).finally(() => clearTimeout(timeout));
}
// awards.ts
const awards = dbApi
.enhanceEndpoints({ addTagTypes: ['Themes'] })
.injectEndpoints({
endpoints: (builder) => ({
getThemes: builder.query<ThemeData[], void>({
async queryFn(arg, api) {
try {
const { auth } = api.getState() as RootState;
const programme = auth.user?.unit.guidingProgramme!;
const path = `/themes/${programme}`;
const themes = await getCollection<ThemeData[]>(path, true);
return { data: themes };
} catch (error) {
return { error: error as FirebaseError };
}
},
providesTags: ['Themes'],
keepUnusedDataFor: 1000 * 60
}),
getTheme: builder.query<ThemeData, string | undefined>({
async queryFn(slug, api) {
try {
const initiate = awards.endpoints.getThemes.initiate;
const getThemes = api.dispatch(initiate());
const { data } = (await getThemes) as ApiResponse<ThemeData[]>;
const name = slug
?.split('-')
.map(
(value) =>
value.substring(0, 1).toUpperCase() +
value.substring(1).toLowerCase()
)
.join(' ');
return { data: data?.find((theme) => theme.name === name) };
} catch (error) {
return { error: error as FirebaseError };
}
},
keepUnusedDataFor: 0
})
})
});

How to map over results in Realm custom resolver function find() response?

I am trying to create a function for my custom resolver that gets all documents in a collection and returns an amended payload with new data. Below is the code that im using to get one client and amend its data:
exports = (input) => {
const clientId = input._id;
const openStatusId = new BSON.ObjectId("898999");
const mongodb = context.services.get("mongodb-atlas");
const clientRecords = mongodb.db("db-name").collection("clients");
const jobRecords = mongodb.db("db-name").collection("jobs");
let client = clientRecords.findOne({"_id": clientId});
const query = { "client_id": clientId};
let jobsForClient = jobRecords.count(query)
.then(items => {
console.log(`Successfully found ${items} documents.`)
// items.forEach(console.log)
return items
})
.catch(err => console.error(`Failed to find documents: ${err}`));
let openJobs = jobRecords.count({"client_id": clientId,"status": openStatusId})
.then(numOfDocs => {
console.log(`Found ${numOfDocs} open jobs.`)
// items.forEach(console.log)
return numOfDocs
})
.catch(err => console.error(`Failed to find documents: ${err}`));
return Promise.all([client, jobsForClient, openJobs]).then(values => {
return {...values[0], "jobs": values[1], "openJobs": values[2]}
})
};
How can i fix this function to get all clients and loop over them to add data to each client?
I understand that changing this:
let client = clientRecords.findOne({"_id": clientId});
to this
let clients = clientRecords.find();
will get all the documents from the clients collection. How would i loop over each client after that?
UPDATE:
I have updated the function to the below and it works when running it in the realm environment but gives me an error when running it as a GraphQL query.
Updated code:
exports = (input) => {
const openStatusId = new BSON.ObjectId("999999");
const mongodb = context.services.get("mongodb-atlas");
const clientRecords = mongodb.db("db-name").collection("clients");
const jobRecords = mongodb.db("db-name").collection("jobs");
const clients = clientRecords.find();
const formatted = clients.toArray().then(cs => {
return cs.map((c,i) => {
const clientId = c._id;
const query = { "client_id": clientId};
let jobsForClient = jobRecords.count(query)
.then(items => {
console.log(`Successfully found ${items} documents.`)
// items.forEach(console.log)
return items
})
.catch(err => console.error(`Failed to find documents: ${err}`));
let openJobs = jobRecords.count({"client_id": clientId,"status": openStatusId})
.then(numOfDocs => {
console.log(`Found ${numOfDocs} open jobs.`)
// items.forEach(console.log)
return numOfDocs
})
.catch(err => console.error(`Failed to find documents: ${err}`));
return Promise.all([jobsForClient, openJobs]).then(values => {
return {...c, "jobs": values[0], "openJobs": values[1]}
});
})
}).catch(err => console.error(`Failed: ${err}`));
return Promise.all([clients, formatted]).then(values => {
return values[1]
}).catch(err => console.error(`Failed to find documents: ${err}`));
};
Error in GraphQL:
"message": "pending promise returned that will never resolve/reject",
It looks like you need wait for the last promise in your function to resolve before the function returns. I would do something like this:
exports = async (input) => {
...
let values = await Promise.all([jobsForClient, openJobs]);
return {...c, "jobs": values[0], "openJobs": values[1]};
}
Managed to solve by using mongodb aggregate. Solution below:
exports = async function(input) {
const openStatusId = new BSON.ObjectId("xxxxxx");
const mongodb = context.services.get("mongodb-atlas");
const clientRecords = mongodb.db("xxxxx").collection("xxxx");
const jobRecords = mongodb.db("xxxxx").collection("xxxx");
return clientRecords.aggregate([
{
$lookup: {
from: "jobs",
localField: "_id",
foreignField: "client_id",
as: "totalJobs"
}
},
{
$addFields: {
jobs: { $size: "$totalJobs" },
openJobs: {
$size: {
$filter: {
input: "$totalJobs",
as: "job",
cond: { "$eq": ["$$job.status", openStatusId]},
}
}
},
}
}
]);
};

What to return if no id found using react and typescript?

i have a query "details" which is like below
query details(
$id: ID!
) {
something(id: $id) {
id
itemId
typesId
itemDetails {
id
name
}
typesDetails {
id
name
}
}
}
i have defined the types like below
type itemDetails {
id: String,
name: String,
}
type typesDetails {
id: String,
name: String,
}
type something {
id: ID!
itemId: ID
typesId: [ID!]
itemDetails: itemDetails
typesDetails: [typesDetails]
}
on the resolvers side (graphql) i have to field resolve the itemDetails (with the itemId i recieve from backend). this itemId can be null or can have some string value like example '1'.
and typesDetails with the typesId i receive from backend. typesId can be null or array of ids like example ['1','2',...]
const resolvers: Resolvers = {
something: {
itemDetails: async(parent, args, { dataSources: { itemsAPI} }) => {
const itemId = get (parent, 'itemId'); //can be null or string value
if(itemId) {
const { data } = await itemsAPI.getItems();
const item = data.filter((item: any) =>
itemId === item.id
); //filter the data whose id is equal to itemId
return {
id: item[0].id,
name: item[0].name,
}
}else { // how to rewrite this else part
return {}:
}
},
typesDetails: async (parent, args, { dataSources: {assetTypesAPI} }) => {
const typesId = get(parent, 'typesId');
if (typesId) {
const allTypes = await typesAPI.getTypes();
const res = typesId.map((id: any) => allTypes.find((d) => d.id === id)); //filter
//allTypes that match typesId
const final = res.map(({id, name}: {id:string, name:string}) => ({id,name}));
//retreive the id and name fields from res array and put it to final array
return final;
} else { // how to rewrite this else part
return [{}];
}
}
}
The above code works. but the code looks clumsy in the way i return empty array if no itemId and typesId returned from backend.
how can i handle the case for itemDetails field if itemId is null from backend and typesDetails field if typesId is null from backend.
could someone help me with this. thanks.
The question is more about graphql than it is react and typescript. You do well to change the tag 🏷 of the question for proper matching.
The perfect solution for me would be validating the itemId in the case of itemDetails. If the value is null, throw an error or return an empty object like you're doing in the else section of the itemDetails.
I don't think itemId is coming from the backend in the case above, it should be coming from the argument passed to the query itemDetails
The same applies to the typesDetails.
Using try and catch can help catch any error during the async operations to the API (database).
const resolvers: Resolvers = {
something: {
itemDetails: async(parent, args, { dataSources: { itemsAPI} }) => {
try {
const itemId = get (parent, 'itemId'); //can be null or string value
if(itemId) {
const { data } = await itemsAPI.getItems();
const item = data.filter((item: any) =>
itemId === item.id
); //filter the data whose id is equal to itemId
return {
id: item[0].id,
name: item[0].name,
}
}else { // how to rewrite this else part
return {}:
}
} catch(error) {
throw new Error('something bad happened')
}
},
typesDetails: async (parent, args, { dataSources: {assetTypesAPI} }) => {
try {
const typesId = get(parent, 'typesId');
if (typesId) {
const allTypes = await typesAPI.getTypes();
const res = typesId.map((id: any) => allTypes.find((d) => d.id === id)); //filter
//allTypes that match typesId
const final = res.map(({id, name}: {id:string, name:string}) => ({id,name}));
//retreive the id and name fields from res array and put it to final array
return final;
} else { // how to rewrite this else part
return [{}];
}
} catch (error) {
throw new Error('something happened')
}
}
}

Object Destructuring

How can I write this code in more elegant way. I have looked on lodash etc. but couldn't actually find the best way to destructure object for my needs.
Because I will write those properties on mongo I also tried to verify if they are exist or not.
const { _id, name, bio, birth_date, photos, instagram, gender, jobs, schools } = element
let myPhotos = photos.map((photo) => photo.id)
let insta = {}
if (instagram) {
insta.mediaCount = instagram.media_count
insta.profilePicture = instagram.profile_picture
insta.username = instagram.username
insta.photos = instagram.photos.map((photo) => photo.image)
}
const doc = {}
doc._id = ObjectId(_id)
doc.name = name
doc.birthDate = new Date(birth_date)
if (bio.length) {
doc.bio = bio
}
if (myPhotos.length) {
doc.photos = myPhotos
}
if (Object.keys(insta).length) {
doc.instagram = insta
}
doc.gender = gender
if (jobs.length) {
doc.jobs = jobs
}
if (schools.length) {
doc.schools = schools
}
try {
await collection.insertOne(doc)
} catch (error) {
console.log("err", error)
}
You could define the doc all at once using the ternary operator to test conditions. If the undefined properties need to be removed, then you can remove them via reduce afterward.
const { _id, name, bio, birth_date, photos, instagram, gender, jobs, schools } = element
const myPhotos = photos.map(({ id }) => id)
const insta = !instagram ? undefined : (() => {
const { media_count, profile_picture, username, photos } = instagram;
return {
mediaCount: media_count,
profilePicture: profile_picture,
username,
photos: photos.map(({ image }) => image)
}
})();
const docWithUndef = {
_id: ObjectId(_id),
name,
gender,
birthDate: new Date(birth_date),
bio: bio.length ? bio : undefined,
photos: myPhotos.length ? myPhotos : undefined,
instagram: insta,
jobs: jobs.length ? jobs : undefined,
schools: schools.length ? schools : undefined,
}
const doc = Object.entries(docWithUndef)
.reduce((accum, [key, val]) => {
if (val !== undefined) accum[key] = val;
return accum;
});
try {
await collection.insertOne(doc)
} catch (error) {
console.log("err", error)
}
Note the destructuring of the arguments to reduce the syntax noise, and the use of const rather than let (improves code readability).

How to re-order Firestore 'modified' changes using DocumentChange.newIndex?

I'm using the web API for Firestore to perform a simple query ordered on a date property formatted as a string ('2017-12-30'). I use the onSnapshot() method to subscribe as a listener to document changes. The initial population of list of results works as expected - the order is correct.
As I make changes to the data, the callback then gets called with a change type of 'modified'. If any of the changes affects the date property, then I have no way of re-ordering the item in the list of results - unlike the old Realtime Database. That is, until I saw the newIndex and oldIndex properties of DocumentChange. They are undocumented for the Web API (https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentChange), but are documented as part of the Node.js API (https://cloud.google.com/nodejs/docs/reference/firestore/0.10.x/DocumentChange).
So, my problem seemed to be solved - except that in practice the values in newIndex and oldIndex seem to be largely random and bear no relation to the actual order if I refresh the query. I can't make out any pattern that would explain the index values I get back.
Has anyone used DocumentChange.newIndex and DocumentChange.oldIndex successfully? If not, how would you reorder results in subscribers as a result of changes?
const query = firestore.collection(`users/${uid}/things`).
orderBy('sortDate', 'desc').limit(1000)
query.onSnapshot(snapshot => {
snapshot.docChanges.forEach(change => {
if (change.type === "added") {
dispatch(addThing({
id: change.doc.id,
...change.doc.data()
}, change.newIndex)
}
if (change.type === "modified") {
dispatch(changeThing({
id: change.doc.id,
...change.doc.data()
}, change.oldIndex, change.newIndex))
}
if (change.type === "removed") {
dispatch(removeThing(change.doc.id, change.oldIndex))
}
})
})
The original problem I had with the DocumentChange indexes was due to a couple of bugs elsewhere in my code. As I didn't find any examples of this in use outside of the Node.js Firestore docs, here's the test code I used to verify its correct behaviour (ES6). It assumes firebase has been initialized.
cleanTestData = (firestore, path) => {
console.log("Cleaning-up old test data")
var query = firestore.collection(path)
return query.get().then(snapshot => {
const deletePromises = []
if (snapshot.size > 0) {
snapshot.docs.forEach(function(doc) {
deletePromises.push(doc.ref.delete().then(() => {
console.log("Deleted ", doc.id)
}))
});
}
return Promise.all(deletePromises)
}).then(() => {
console.log("Old test data cleaned-up")
})
}
createTestData = (firestore, path) => {
console.log("Creating test data")
const batch = firestore.batch()
const data = {
a: '2017-09-02',
b: '2017-12-25',
c: '2017-10-06',
d: '2017-08-02',
e: '2017-09-20',
f: '2017-11-17'
}
for (const id in data) {
batch.set(firestore.collection(path).doc(id), { date: data[id] })
}
return batch.commit().then(() => {
console.log("Test data created");
}).catch(error => {
console.error("Failed to create test data: ", error);
})
}
subscribe = (firestore, path) => {
const datesArray = []
return firestore.collection(path).orderBy('date', 'asc').onSnapshot(snapshot => {
snapshot.docChanges.forEach(change => {
console.log(change.type, "id:", change.doc.id,
"; date:", change.doc.data().date,
"; oldIndex:", change.oldIndex, "; newIndex:", change.newIndex,
"; metadata: ", change.doc.metadata)
if (change.oldIndex !== -1) {
datesArray.splice(change.oldIndex, 1);
}
if (change.newIndex !== -1) {
datesArray.splice(change.newIndex, 0, change.doc.data().date);
}
console.log(" -->", JSON.stringify(datesArray))
})
})
}
update = (firestore, path) => {
console.log("Updating test data")
return firestore.collection(path).doc('d').set({date: '2018-01-02'}).then(() => {
console.log("Test doc 'd' updated from '2017-08-02' to '2018-01-02'")
})
}
query = (firestore, path) => {
var query = firestore.collection(path).orderBy('date', 'asc')
return query.get().then(snapshot => {
const dates = []
if (snapshot.size > 0) {
snapshot.docs.forEach(function(doc) {
dates.push(doc.data().date)
});
}
console.log("Fresh query of data: \n -->", JSON.stringify(dates))
})
}
handleStartTest = e => {
console.log("Starting test")
const firestore = firebase.firestore()
const path = `things`
let unsubscribeFn = null
unsubscribeFn = this.subscribe(firestore, path)
this.cleanTestData(firestore, path).then(() => {
return this.createTestData(firestore, path)
}).then(() => {
return this.update(firestore, path)
}).then(() => {
return this.query(firestore, path)
}).then(() => {
unsubscribeFn()
console.log("Test complete")
}).catch((error) => {
console.error("Test failed: ", error)
})
}
This is the way it worked for me:
onSnapshot((ref) => {
ref.docChanges().forEach((change) => {
const { newIndex, oldIndex, doc, type } = change;
if (type === 'added') {
this.todos.splice(newIndex, 0, doc.data());
// if we want to handle references we would do it here
} else if (type === 'modified') {
// remove the old one first
this.todos.splice(oldIndex, 1);
// if we want to handle references we would have to unsubscribe
// from old references' listeners and subscribe to the new ones
this.todos.splice(newIndex, 0, doc.data());
} else if (type === 'removed') {
this.todos.splice(oldIndex, 1);
// if we want to handle references we need to unsubscribe
// from old references
}
});
});
source

Categories