0

I'm trying to work around the fact that Datocms doesn't support a where filter in their GraphQL schema. Since there isn't that much data, I figured I could query all of it, and do the find on my end, but ... I'm not succeeding, at least not using "modern" methods.

What I get back when I query all of the data looks like this:

"foo": {
 "data": {
 "allGiveawayLandingPages": [
 {
 "lpSection": [
 {},
 {},
 {},
 {},
 {},
 {},
 {},
 {
 "id": "34525949",
 "products": [
 {
 "__typename": "PurchaseCardRecord",
 "discountAmount": 50,
 "discountAmountPct": null,
 "discountEndDate": "2022年11月01日T23:00:00+00:00",
 "id": "44144096"
 },
 {
 "__typename": "PurchaseCardRecord",
 "discountAmount": null,
 "discountAmountPct": null,
 "discountEndDate": null,
 "id": "44144097"
 }
 ]
 }
 ]
 }
 ]
 }
}

I need to find the object down in the "products" array by "id". This general question has been asked and answered lots of times, but the only answer I can get to work is from way back in 2013, and it seems to me there aught to be a more modern way to do it.

I'm doing this inside of a try/catch block, which I mention because it seems to be making this hard to debug (I'll come back to this):

export default async function createPaymentIntentHandler(req, res) {
 const body = JSON.parse(req.body);
 const {
 productId,
 productType
 } = body;
 let data;
 if ('POST' === req.method) {
 try {
 switch (productType) {
 case 'SeminarRecord':
 data = await request({ query: singleSeminarQuery(productId) });
 productObjName = 'seminar';
 break;
 default:
 data = await request({ query: singleProductQuery(productId) });
 productObjName = 'product';
 }
 /**
 * Here's where I want to do my query / filtering
 */
 // ... do more stuff and create Stripe paymentIntent
 res.status(200).send({clientSecret: paymentIntent.client_secret})
 } catch (error) {
 logger.error({error}, 'Create Payment Intent error');
 return res.status(400).end(`Create Payment Intent error: ${error.message}`);
 }
 } else {
 res.status(405).end('Method not allowed');
 }
}

My first, naive attempt was

const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = foo.data.allGiveawayLandingPages.find((page) => {
 return page.lpSection.find((section) => {
 return section?.products.find((record) => record.id === parentId)
 })
});
logger.debug({card}, 'Got card');

In the abstract, aside from the fact that the above is fairly brittle because it relies on the schema not changing, I'd expect some similar sort of ES6 construction to work. This particular one, however, throws, but not in a particularly useful way:

[08:09:18.690] ERROR: Create Payment Intent error
 env: "development"
 error: {}

That's what I meant by it being hard to debug — I don't know why the error object is empty. But, in any case, that's when I started searching StackOverflow. The first answer which looked promising was this one, which I implemented as

...
const {
 productId,
 productType,
 parentId
} = body;
...
function findCard(parent, id) {
 logger.debug({parent}, 'searching in parent')
 for (const item of parent) {
 if ('PurchaseCardRecord' === item.__typename && item.id === id) return item;
 if (item.children?.length) {
 const innerResult = findCard(item.children, id);
 if (innerResult) return innerResult;
 }
 }
}
if ('POST' === req.method) {
 try {
 ...
 const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
 const card = findCard(foo, parentId);
 logger.debug({card}, 'Got card');

This similarly throws unhelpfully, but my guess is it doesn't work because in the structure, not all children are iterables. Then I found this answer, which uses reduce instead of my original attempt at find, so I took a pass at it:

 const card = foo.data.allGiveawayLandingPages.reduce((item) => {
 item?.lpSection.reduce((section) => {
 section?.products.reduce((record) => {
 if ('PurchaseCardRecord' === record.__typename && record.id === parentId) return record;
 })
 })
 })

This is actually the closest I've gotten using ES6 functionality. It doesn't throw an error; however, it's also not returning the matching child object, it's returning the first parent object that contains the match (i.e., it's returning the whole "lpSection" object). Also, it has the same brittleness problem of requiring knowledge of the schema. I'm relatively certain something like this is the right way to go, but I'm just not understanding his original construction:

arr.reduce((a, item) => {
 if (a) return a;
 if (item.id === id) return item;

I've tried to understand the MDN documentation for Array.reduce, but, I don't know, I must be undercaffeinated or something. The syntax is described as

reduce((previousValue, currentValue) => { /* ... */ } )

and then several variations on the theme. I thought it would return all the way up the stack in my construction, but it doesn't. I also tried

 const card = foo.data.allGiveawayLandingPages.reduce((accumulator, item) => {
 return item?.lpSection.reduce((section) => {
 return section?.products.reduce((record) => {
 if ('PurchaseCardRecord' === record.__typename && record.id === parentId) return record;
 })
 })
 })

but the result was the same. Finally, not understanding what I'm doing, I went back to an older answer that doesn't use the ES6 methods but relies on recursing the object.

...
function filterCards(object) {
 if (object.hasOwnProperty('__typename') && object.hasOwnProperty('id') && ('PurchaseCardRecord' === object.__typename && parentId === object.id)) return object;
 for (let i=0; i<Object.keys(object).length; i++) {
 if (typeof object[Object.keys(object)[i]] == 'object') {
 const o = filterCards(object[Object.keys(object)[i]]);
 if (o != null) return o;
 }
 }
 return null;
}
if ('POST' === req.method) {
 try {
 ...
 const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
 const card = filterCards(foo);
 logger.debug({card}, 'Got card');

This actually works, but ISTM there should be a more elegant way to solve the problem with modern Javascript. I'm thinking it's some combination of .find, .some, and .reduce. Or maybe just for ... in.

I'll keep poking at this, but if anyone has an elegant/modern answer, I'd appreciate it!

asked Oct 31, 2022 at 9:02
5
  • Do you need the product Objects or the lpSection Objects which contain such products ? The Payment Intent error probably comes from Stripe SDK and not from your own code. Commented Oct 31, 2022 at 11:44
  • I need the object inside of the "products" array which matches my id (but everything above and below it will have an id, and I'm not sure DatoCMS guarantees uniqueness across models, which is why I'm checking for __typename too. As for the error, the words 'Create Payment Intent error' are mine (in the logging call); it's never getting to the stripe call (I know, because it's not emitting logs before Stripe is called). Commented Oct 31, 2022 at 12:01
  • 1
    Then you can try foo.data.allGiveawayLandingPages.reduce((accPage, page) => accPage.concat(page.lpSection.reduce((accSection, section) => accSection.concat((section.products || []).filter(product => product.__typename === 'PurchaseCardRecord')), [])), []) Commented Oct 31, 2022 at 12:11
  • That does, in fact, work (updated to match on parentID for id), but ... I'm not sure it helps improve anything (in terms of clarity or maintainability, etc...) :) Thanks, though! Commented Oct 31, 2022 at 15:57
  • Well, this is the JSON that you receive from the backend. The data is nested deeply - so you are unnesting it. You may also look at Array.flat() or Array.flatMap() if you want to try something more fancy. Commented Nov 1, 2022 at 8:34

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.