Strange 422 on client.items.create()

Hi there!
Iā€™m writing a cli tool that will clone some Dato objects from one project to another, Iā€™m using the latest datocms-client for node (3.5.6) and Iā€™m stuck on some validation errors that I keep gettingā€¦

If I try to create the complete object, I get this error:

seems like the postCover attribute is missing even though you can see the object Iā€™m trying to create below has a proper object created with the helper buildModularBlock.

 {
  itemType: '418355',
  title: 'Test TK',
  abstract: '<p>Tk</p>',
  category: '11414710',
  postCover: [ { type: 'item', attributes: [Object], relationships: [Object] } ],
  previewImage: { uploadId: '28382383' },
  slug: 'tk-test',
  seoFields: { title: 'TK', description: 'TK', image: '28382383' },
  relatedPosts: [],
  tags: [],
  importToCloud: false,
  showSeeAlso: false,
  publishedAuthorName: 'Nome autore pubblicato',
  editorName: 'Nome autore pubblicato',
  originalPublicationDate: '2021-10-26T10:54:08.406-04:00',
  originalPostId: 'usa:24560128',
  previewLink: null,
  postBlocks: [ { type: 'item', attributes: [Object], relationships: [Object] } ]
}

This is the complete error Iā€™m getting from the client:

ApiException: 422 MISSING_FIELDS (details: {"required_fields":["cover_image"],"missing_fields":["cover_image"],"locale":null})
    at /xxx/node_modules/datocms-client/lib/Client.js:224:33
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at async /xxx/src/routes/syndicator.js:1281:32
Caused By:
Error
    at Object.get (/xxx/node_modules/datocms-client/lib/utils/generateClient.js:123:28)
    at /xxx/src/routes/syndicator.js:1281:62 {
  body: { data: [ [Object], [Object], [Object] ] },
  headers: [Object: null prototype] {
    date: [ 'Tue, 26 Oct 2021 15:12:07 GMT' ],
    'content-type': [ 'application/json; charset=utf-8' ],
    'transfer-encoding': [ 'chunked' ],
    connection: [ 'close' ],
    'cf-ray': [ '6a449f001920e8f7-MXP' ],
    'access-control-allow-origin': [ '*' ],
    'cache-control': [ 'no-cache' ],
    'content-encoding': [ 'gzip' ],
    'strict-transport-security': [ 'max-age=15552000; includeSubDomains; preload' ],
    vary: [ 'Accept,Accept-Encoding' ],
    via: [ '1.1 vegur' ],
    'cf-cache-status': [ 'DYNAMIC' ],
    'access-control-allow-credentials': [ 'true' ],
    'access-control-allow-headers': [
      'authorization, content-type, x-environment, x-site-domain, x-api-version, user-agent, x-session-id'
    ],
    'access-control-allow-methods': [ 'GET, POST, PUT, OPTIONS, DELETE' ],
    'access-control-expose-headers': [ 'x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset' ],
    'access-control-max-age': [ '1728000' ],
    'expect-ct': [
      'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"'
    ],
    'referrer-policy': [ 'strict-origin-when-cross-origin' ],
    'x-api-version': [ '3' ],
    'x-content-type-options': [ 'nosniff' ],
    'x-download-options': [ 'noopen' ],
    'x-environment': [ 'main' ],
    'x-frame-options': [ 'SAMEORIGIN' ],
    'x-permitted-cross-domain-policies': [ 'none' ],
    'x-queue-time': [ '0ms' ],
    'x-ratelimit-limit': [ '60' ],
    'x-ratelimit-remaining': [ '56' ],
    'x-request-id': [ '220c3f80-6548-4277-85df-0dd0ba04105d' ],
    'x-runtime': [ '0.080271' ],
    'x-xss-protection': [ '1; mode=block' ],
    server: [ 'cloudflare' ]
  },
  statusCode: 422,
  statusText: 'Unprocessable Entity',
  requestUrl: 'https://site-api.datocms.com/items',
  requestMethod: 'POST',
  requestHeaders: {
    'content-type': 'application/json',
    accept: 'application/json',
    authorization: 'Bearer xxx',
    'user-agent': 'js-client v3.5.6',
    'X-Api-Version': '3'
  },
  requestBody: '{"data":{"type":"item","attributes":{"title":"Test TK","abstract":"<p>Tk</p>","category":"11414710","post_cover":[{"type":"item","attributes":{"cover_image":{"upload_id":"28382385"}},"relationships":{"item_type":{"data":{"id":"418358","type":"item_type"}}}}],"preview_image":{"upload_id":"28382383"},"slug":"tk-test","seo_fields":{"title":"TK","description":"TK","image":"28382383"},"related_posts":[],"tags":[],"import_to_cloud":false,"show_see_also":false,"published_author_name":"Nome autore pubblicato","editor_name":"Nome autore pubblicato","original_publication_date":"2021-10-26T10:54:08.406-04:00","original_post_id":"usa:24560128","preview_link":null,"post_blocks":[{"type":"item","attributes":{"content":["<p><span>Il generatore di toni online puĆ² essere utilizzato per creare segnali Dual Tone Multi Frequency (DTMF) comunemente ascoltati sui tasti del telefono. Ogni tono ĆØ semplicemente la somma di due onde sinusoidali. Fare clic e tenere premuti i pulsanti del tastierino per ascoltare ogni tono.</span></p>"]},"relationships":{"item_type":{"data":{"id":"418358","type":"item_type"}}}}]},"relationships":{"item_type":{"data":{"type":"item_type","id":"418355"}}}}}'
}

If instead, I try to create the object without the postBlocks field, the creation went through and I get no 422 errors back, just plain beautiful 200. :slight_smile:

This is the object Iā€™m able to create:

 {
  itemType: '418355',
  title: 'Test TK',
  abstract: '<p>Tk</p>',
  category: '11414710',
  postCover: [ { type: 'item', attributes: [Object], relationships: [Object] } ],
  previewImage: { uploadId: '28382950' },
  slug: 'tk-test',
  seoFields: { title: 'TK', description: 'TK', image: '28382950' },
  relatedPosts: [],
  tags: [],
  importToCloud: false,
  showSeeAlso: false,
  publishedAuthorName: 'Nome autore pubblicato',
  editorName: 'Nome autore pubblicato',
  originalPublicationDate: '2021-10-26T10:54:08.406-04:00',
  originalPostId: 'usa:24560128',
  previewLink: null,
  postBlocks: [] // <--- THIS IS THE DIFFERENCE
}

As you can see the postCover attribute is the same on both tests, the only thing that changes and prevents the item creation is the content of postBlocks.

Any help would be really appreciated because Iā€™ve tried dozens of times and Iā€™m stuck.

Thank you very much,
Cheers!

This might be helpful to point me in the right directionā€¦ This is the piece of the Express app Iā€™m coding to duplicate the object across projects. It is meant to be used via a POST API call.

As you can see at the beginning there are a lot of async functions that handle the translation, asset copying across media galleries, and so on.

Then thereā€™s a Promise.all that is supposed to guarantee that all the previous async functions have been handled.

Finally, thereā€™s the actual piece of code that is supposed to create the object (itemToBeCreated) on the destination project.


router.post("/", async (req, res) => {
  // #   sourceTenant: "usa",
  // #   sourceLocale: "en-US",
  // #   postId: 26445413,
  // #   destinationTenant: "italy"

  try {
    const { sourceTenant, sourceLocale, postId, destinationTenant } = req.body;
    const sourceTenantClient = new SiteClient(getTenantApiKey(sourceTenant));
    const destinationTenantClient = new SiteClient(
      getTenantApiKey(destinationTenant)
    );
    const sourcePost = await getSingleArticle(sourceTenantClient, postId);
    const destinationcategory = await getFirstAvailableCategory(
      destinationTenantClient
    );
    const slugExistance = await slugExists(
      destinationTenantClient,
      sourcePost[0]["slug"]
    );

    if (slugExistance) {
      res.status(500).json("Slug already exists!");
    } else {
      // Assign the article to the first available category on destination tenant
      const destinationcategoryId = destinationcategory[0]["id"];
      const creatorFullName = await getUserFullName(
        sourceTenantClient,
        sourcePost[0]["creator"]["id"]
      );
      const publishedAuthorName =
        sourcePost[0]["publishedAuthorName"] === ""
          ? creatorFullName
          : sourcePost[0]["publishedAuthorName"];

      // Handle Preview Image
      const previewImageAsset = await findUpload(
        sourceTenantClient,
        sourcePost[0]["previewImage"]["uploadId"]
      );
      const previewImageUrl = previewImageAsset[0]["url"];
      const previewImageOnDato = await createUpload(
        destinationTenantClient,
        previewImageUrl
      );

      const destinationLocale = await getSiteLocale(destinationTenantClient);

      const cover = await handleCoverImageOrVideo(
        sourceTenant,
        sourceTenantClient,
        destinationTenant,
        destinationTenantClient,
        sourcePost
      );

      // Handle post blocks
      const handledPostBlocks = await handlePostBlocks(
        sourceTenant,
        sourceTenantClient,
        sourceLocale,
        destinationTenant,
        destinationTenantClient,
        destinationLocale[0],
        sourcePost[0]["postBlocks"]
      );

      const translatedTitle = await translateText(
        sourcePost[0]["title"],
        sourceLocale,
        destinationLocale[0]
      );
      const truncatedTranslatedTitle = translatedTitle[0].substring(0, 89);

      const translatedAbstract = await translateText(
        sourcePost[0]["abstract"],
        sourceLocale,
        destinationLocale[0],
        "text/html"
      );

      const translatedSlug = await translateText(
        sourcePost[0]["slug"],
        sourceLocale,
        destinationLocale[0]
      );

      const parameterizedTranslatedSlug = parameterizeString(translatedSlug[0]);

      const translatedSeoTitle = await translateText(
        sourcePost[0]["seoFields"]["title"],
        sourceLocale,
        destinationLocale[0]
      );
      const truncatedSeoTitle = translatedSeoTitle[0].substring(0, 89);

      const translatedSeoDescription = await translateText(
        sourcePost[0]["seoFields"]["description"],
        sourceLocale,
        destinationLocale[0]
      );
      const truncatedSeoDescription = translatedSeoDescription[0].substring(
        0,
        159
      );
      const originalPublicationDate =
        sourcePost[0]["meta"]["publishedAt"] === null
          ? sourcePost[0]["meta"]["createdAt"]
          : sourcePost[0]["meta"]["publishedAt"];

      const originalPostId = `${sourceTenant}:${postId}`;

      Promise.all([
        creatorFullName,
        previewImageAsset,
        previewImageOnDato,
        destinationLocale,
        translatedAbstract,
        translatedSlug,
        cover,
        translatedSeoTitle,
        translatedSeoDescription,
        handledPostBlocks,
      ]).then(async () => {
        const itemToBeCreated = {
          itemType:
            jsonSecrets[destinationTenant.toString()]["datocms_post_model_id"],
          title: truncatedTranslatedTitle,
          abstract: translatedAbstract[0],
          category: destinationcategoryId,
          postCover: cover,
          previewImage: {
            uploadId: previewImageOnDato,
          },
          slug: parameterizedTranslatedSlug,
          seoFields: {
            title: truncatedSeoTitle,
            description: truncatedSeoDescription,
            image: previewImageOnDato,
          },
          relatedPosts: [],
          tags: [],
          importToCloud: false,
          showSeeAlso: false,
          publishedAuthorName: publishedAuthorName,
          editorName:
            sourcePost[0]["editorName"] === null ||
            sourcePost[0]["editorName"] === ""
              ? publishedAuthorName
              : sourcePost[0]["editorName"],
          originalPublicationDate: originalPublicationDate,
          originalPostId: originalPostId,
          previewLink: null,
          postBlocks: handledPostBlocks,
        };
        console.log("itemToBeCreated: \n", itemToBeCreated);
        try {
          const clonedRecord = await destinationTenantClient.items.create(
            itemToBeCreated
          );
          if (clonedRecord[""] != []) {
            res.status(200).json(clonedRecord);
          } else {
            res.status(500).json("Error importing Article!");
          }
        } catch (error) {
          console.error(error);
        }
      });
    }
  } catch (error) {
    console.error(error);
  }
});

Iā€™m really having a hard time understanding why if I pass itemToBeCreated with postBlocks filled I get a 422 on another field (cover_image)ā€¦

Thanks for any help!

Ehi @g.demartino, please send us the complete item thatā€™s failing, as you can see here there are some inner properties redacted ([Object]) so we have no way to reproduce the issue.

Thanks @s.verna , here you are:

{
  "data": {
    "type": "item",
    "attributes": {
      "title": "Test TK",
      "abstract": "<p>Tk</p>",
      "category": "11414710",
      "post_cover": [
        {
          "type": "item",
          "attributes": { "cover_image": { "upload_id": "28585282" } },
          "relationships": {
            "item_type": { "data": { "id": "418358", "type": "item_type" } }
          }
        }
      ],
      "preview_image": { "upload_id": "28585279" },
      "slug": "tk-test",
      "seo_fields": { "title": "TK", "description": "TK", "image": "28585279" },
      "related_posts": [],
      "tags": [],
      "import_to_cloud": false,
      "show_see_also": false,
      "published_author_name": "Nome autore pubblicato",
      "editor_name": "Nome autore pubblicato",
      "original_publication_date": "2021-10-27T08:19:16.469-04:00",
      "original_post_id": "usa:24560128",
      "preview_link": null,
      "post_blocks": [
        {
          "type": "item",
          "attributes": {
            "content": [
              "<p><span>prova prova test.</span></p>"
            ]
          },
          "relationships": {
            "item_type": { "data": { "id": "418358", "type": "item_type" } }
          }
        }
      ]
    },
    "relationships": {
      "item_type": { "data": { "type": "item_type", "id": "418355" } }
    }
  }
}

And it returns:

ApiException: 422 MISSING_FIELDS (details: {ā€œrequired_fieldsā€:[ā€œcover_imageā€],ā€œmissing_fieldsā€:[ā€œcover_imageā€],ā€œlocaleā€:null})

Hey @g.demartino the block 418358 (the Cover Image block) has a field named cover_image that is an asset, instead you are passing a string field named content.

Thatā€™s why you are getting a validation error saying that the cover_image field is missing.

Hope this clarifies?

ARHGHHHHHHHHH!!!

Damn me and damn copy-pasta! :sob:

Hereā€™s the bug:

const createTextAreaPostBlock = async (
  textArea,
  sourceLocale,
  destinationLocale,
  destinationTenant
) => {
  if (textArea["content"] !== "") {
    return buildModularBlock({
      itemType:
        jsonSecrets[destinationTenant.toString()][
          "datocms_cover_image_block_id"
        ], // <-- THIS is not the right ID!!!

      content:
        textArea["content"] === null
          ? null
          : await translateText(
              textArea["content"],
              sourceLocale,
              destinationLocale,
              "text/html"
            ),
    });
  }
};

Gonna fix this and try again and let you know :+1:t2:

1 Like

Thanks, @mat_jack1 that worked! Iā€™m able to create the item on the destination project :+1:t2:

Now Iā€™m facing a different issue, related to the async nature of this procedureā€¦ my source post has two building modular content blocks: one text area and one image box. Even though the bodyā€™s building blocks are treated exactly the same way, itā€™s obvious that translating a Textarea requires less time than uploading an image gallery.

Once the procedure finishes with a 200 OK, I can see the item created on the destination project with just the translated Text Area and not the Image box block.

Iā€™m wondering if thereā€™s another way to make sure that the function handlePostBlocks completely finished before trying to create the item on the destination project.

Iā€™ve added two console.log on the async function responsible for handling the Text Area translation and the Image Gallery and you can see that the text areaā€™s one completes, then I get the 200 OK as if the item creation went through fine (and it has!) and then I get the log from the Image Box handling procedure:

textAreaModularBlock:  {
  itemType: '418371',
  content: '<p><span>Questo ĆØ un testo di esempio tradotto</span></p>'
}

POST /syndicator 200 14960.523 ms - 1019

imageBoxModularBlock:  {
  itemType: '418370',
  image: { alt: null, uploadId: '28590058', title: [ 'Isabella Marant.' ] },
  mode: 'Inside Wrap',
  caption: [ 'Questa ĆØ la didascalia' ],
  externalUrl: 'https://www.google.it'
}

Apparently the buildModularBlock for the Image Box arrives too late when the item has already been created.

But I was hoping that Promise.all would save me from these kinds of conditions.

      Promise.all([
        creatorFullName,
        previewImageAsset,
        previewImageOnDato,
        destinationLocale,
        translatedAbstract,
        translatedSlug,
        cover,
        translatedSeoTitle,
        translatedSeoDescription,
        handledPostBlocks, // <-- THIS is the function that handles all the different types of body building blocks
      ]).then(async () => { ... }) 

Let me know if you have any ideas. Thank you very much!

You mean that the record is created in the then block of the Promise.all?

Can you please share a full example script?

Yes! Thatā€™s why I was sure that all the building blocks should be ready to be added to the destination project. Thatā€™s not the case instead.

Iā€™ll share the repo with you on GH if you could take a lookā€¦!