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