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-01T23: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!

philolegein
  • 1,099
  • 10
  • 28
  • 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. – IVO GELOV Oct 31 '22 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). – philolegein Oct 31 '22 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')), [])), [])` – IVO GELOV Oct 31 '22 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! – philolegein Oct 31 '22 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. – IVO GELOV Nov 01 '22 at 08:34

0 Answers0