How do I update the structure via the api? And how do I bulk-delete records in a tree-like model? [edited title]

Describe the issue:

Iā€™m trying to convert articles from another (not DatoCMS) cms to DatoCMS. Converting the articles is ok, but I also want to put the into a structure like this:

My problem is that Iā€™m not able to figure out how to update the structure. In my first try I just try to insert the single ā€œMin kontoā€ in the example.

My code looks like this:

The data Iā€™m trying to insert is this:

And this is the error:

I do understand that the format is invalid, but I donā€™t understand what is wrong.

The final result should be something like this I guess:

But the first level (ā€œKundeserviceā€) already exists, so Iā€™m trying to create the second level (ā€œMin kontoā€). I will then manually place the second and next levels under ā€œKundeserviceā€ in DatoCMS.

I also have tryed to insert data like this:

But then I get this error:

(Recommended) Whatā€™s the URL of the record or model in question?

  • Being able to see the issue in your real project is you for very helpful for troubleshooting. If you donā€™t provide the URL upfront, weā€™ll often have to ask you for it anyway, which just slows down our response time for you.
  • If youā€™re concerned about privacy, you can email us at support@datocms.com instead of posting here.

(Optional) Do you have any sample code you can provide?

Hi @lasse.norgreen,

Could you please share with me the URL for this model so I can make a proper example for you? Without knowing its exact schema (what its fields are), itā€™s hard to know what exactly itā€™s expecting. Itā€™s probably something to do with some combination of tree-like models, modular content fields, and links to other records, but I canā€™t say for sure without seeing the actual project & model in question.

Just to get you started, though, it looks like you would need some combination of these examples:

  1. Creating new records in a tree-like structure
  2. Using the buildBlockRecord() util to help you make a block, if youā€™re using a modular content field
  3. Linking to another record by its ID, if one of the fields inside that block references another record

If thatā€™s not enough, could you please send me the URL and Iā€™d be happy to take a look and give you an actual, working example? (With your permission, Iā€™ll make a test record there and show you the script?)

You can either DM here on the forum (or just post the URL since itā€™s password-protected anyway), or email us at support@datocms.com and reference this thread.

Thank you! Happy to look into it more once I get the URL :slight_smile:

https://selskapskommunikasjon.admin.datocms.com/environments/kundeservice-2024-12-11/editor/item_types/889494/items?filter[fields][_created_at][gt]=2025-01-02T23%3A00%3A00Z&perPage=200

I have tryed to use Your sample like this:

import { buildClient } from "@datocms/cma-client-node";

const token ='----------------1079';
const environment = 'kundeservice-2024-12-11';
export const itemTypePageStructure = '889821';

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  // const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
const client = buildClient({ apiToken: token, environment });

  const parent = await client.items.create({
    item_type: { type: "item_type", id: itemTypePageStructure },
    name: "Parent",
  });

  console.log(parent);
}

run();

Getting this error:


[Moderator note: Added code formatting]

Hi @lasse.norgreen,

Thanks for the details! Could you please try this instead:

import { buildClient } from "@datocms/cma-client-node";

const token = process.env.DATOCMS_API_TOKEN; // In env var
const environment = 'datocms-support-kundeservice-copy'; // Forked sandbox for testing
export const itemTypePageStructure = '889821';

async function run() {
    const client = buildClient({ apiToken: token, environment });

    const parent = await client.items.create({
        item_type: { type: "item_type", id: itemTypePageStructure },

        // To place it in the tree, specify the parent ID
        parent_id: 'YIeHxlFNTLqbYHQ7fDCpnQ', // ID of the "Kundeservice" record in this model

        // Remove the invalid field. You don't have a "name" field in this model.
        // name: 'Parent',

        // Instead, since your schema specifies a single-link field, you provide the linked record's ID instead
        // That gets used as the title field (per your schema config)
        site_structure_content: // The "Innhold" field's API key.
            'TNsfRzsrRSKXhu6rpg8DGg' // ID of the "Min konto" record of the "modular_page" model
    });

    console.log(parent);
}

run();

That should work:

Explanation:

  1. Your site_structure model (Sidestrucktur) did not have a name field. In its model presentation settings, the name comes from the Innhold field:

  2. By providing the parent_id of the ā€œKundeserviceā€ record of that model, you tell the API you want to create a new record under it.

  3. Then your ā€œInnholdā€ field is a single-link field with the API key site_structure_content. By providing the record ID of the ā€œMin kontoā€ record in the other model, you tell the API you want to create a link to that other record.

Does that help?

Note: Forked sandbox

Please note that to protect your original environment, I forked it to a new sandbox for testing called datocms-support-kundeservice-copy: https://selskapskommunikasjon.admin.datocms.com/environments/datocms-support-kundeservice-copy/editor/item_types/889821/items/YIeHxlFNTLqbYHQ7fDCpnQ/edit

If the script works for you, you can delete that new env and the associated role and API key (all prefixed with ā€œDatoCMS Supportā€).

Or let me know and I can clean them up for you :slight_smile:

Fantastic! Now it works :smiley:
Thankā€™s a lot :blush:

1 Like

Great! Youā€™re welcome :slight_smile: Let me know if youā€™d like me to clean up the stuff I made for testing (the new sandbox env, role, and API key for testing).

I can clean it up myself. But one more question now when everything works: Is there any way to delete all of the structure under one node? I.e. delete all subnodes under ā€œKundeserviceā€. It would be nice when developing and testing :grin:

Itā€™s not a built-in we have, but you can use something like this:

import {buildClient} from "@datocms/cma-client-node";

const token = process.env.DATOCMS_API_TOKEN; // In env var. Needs CMA write access.
const parentRecordId = 'TenAl6DZSOmoJdWbrZZ3rA'; // The record to start deletion from

async function run() {
    const client = buildClient({apiToken: token});

    // Get parent record by ID
    const parentRecord = await client.items.find(parentRecordId)

    // Get model ID from response
    const modelId = parentRecord.item_type.id;

    // Empty placeholder
    let allRecordsOfModel = [];

    // Get all records of that model with a listPagedIterator: https://www.datocms.com/docs/content-management-api/resources/item/instances#all_pages
    // We need to download them all for clientside processing because we can't retrieve its children automatically
    for await (const record of client.items.listPagedIterator({
        filter: {
            type: modelId
        }
    })) {
        allRecordsOfModel.push(record);
    }

    // From all records, look for the uppermost parent (the root) then construct a tree of its children
    const tree = buildTreeFromRoot(allRecordsOfModel, parentRecordId);

    // Also make a flat list of records to delete. It's not technically necessary for this to be a separate step,
    // but constructing a tree beforehand is clearer (and can be reused for other operations).
    const recordsToDelete = flattenTreeIds(tree)

    console.log(`Deleting ${recordsToDelete.length} records starting from '${parentRecordId}'...`);

    // Destroy all the records in bulk, up to 200 max.
    await client.items.bulkDestroy({items: recordsToDelete.map(id => ({type: 'item', id}))})
}


/****** Helper funcs *******/

/**
 * Builds a nested tree structure from a flat array of records.
 *
 * @param {Array} records - Array of records, each containing at least `id` and optional `parent_id`.
 * @param {string} rootId - The ID of the root record to start building the tree from.
 * @returns {Object} The nested tree structure starting from the specified root.
 */
const buildTreeFromRoot = (records, rootId) => {
    // Step 1: Create a map of records by their ID for quick lookup
    const recordById = Object.fromEntries(
        records.map((record) => [record.id, record])
    );

    // Step 2: Create a map that associates each parent ID with its child record IDs
    let childrenByParentId = {};
    records.forEach((record) => {
        const parentId = record.parent_id ? record.parent_id : null; // Use null if no parent

        // Initialize the children array for this parent ID if it doesn't exist
        if (!childrenByParentId[parentId]) {
            childrenByParentId[parentId] = [];
        }

        // Add the current record's ID to its parent's children array
        childrenByParentId[parentId].push(record.id);
    });

    // Step 3: Recursively build the nested tree
    function nestChildren(currentId) {
        const currentRecord = recordById[currentId];

        // Retrieve the children IDs for the current record, or an empty array if none
        const childrenIds = childrenByParentId[currentId] ? childrenByParentId[currentId] : [];

        // Build the children array by recursively nesting each child
        const children = [];
        childrenIds.forEach((childId) => {
            const childTree = nestChildren(childId);
            children.push(childTree);
        });

        // Return the current record with its nested children
        return {
            ...currentRecord,
            children: children,
        };
    }

    // Step 4: Start building the tree from the root ID
    const tree = nestChildren(rootId);

    return tree;
}

/**
 * Recursively flattens a tree structure into a one-dimensional array of IDs.
 *
 * @param {Object} tree - The tree to extract IDs from
 *
 * @returns {string[]} An array containing all `id` values from the root and all child nodes.
 */
const flattenTreeIds = (tree) => {
    // If the node has children, recursively flatten them too
    const childrenPart = tree.children?.flatMap(flattenTreeIds) ?? [];

    // Return an array consisting of the root's ID + all descendant IDs
    return [tree.id, ...childrenPart];
}

run();
1 Like