How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Sync values from another system into custom fields

Read the record type's field definitions from the input types, map the other system's attributes to field names, and send only the fields that changed as customFields on the record's edit route. Fields you do not name keep their values; read the record again to see what is stored.

Context

Context. An ERP holds each product's material and care sheet, a PIM holds a menu item's badge text, and the site has custom fields for them. Custom fields belong to the record, not to a revision, so a write changes the live record at once. Products, product categories, menus and menu elements, users, people, categories, books and a few other types have them; pages, blog posts and custom entity records do not.

Before you start

The custom fields created in the admin for the record type, credentials for a backend user who may edit those records, and a way to find each record from the other system's key.

Steps

  1. List the fields once: GET /cdna-api/common/input-types with customFields.entity set to the entity class, such as Products\Entities\Product. Each field's name, its input type and, for a select, its option keys come back.
  2. Map each attribute from the other system to a field name, and convert the value to what the input type takes: the option key for a select, true for a switch, an id for an image or file (upload the file first), a record id or a list of ids for a record field.
  3. Read the record's current values with customFieldContent and drop every field whose value has not changed.
  4. Send the rest as customFields, keyed by name, on the record's ordinary edit route: PUT /cdna-api/products/product/{id}, PUT /cdna-api/menus/element/{id}. The fields you do not name keep their values. null empties one.
  5. Read the record again if you need to confirm what is stored: the write's answer leaves out any field it emptied.
  6. On a 422, look at the key in validationErrors: customFields.<name> is a field name that does not exist for this type, and image or file is an id that does not exist.

Code

HTTP Real answers from this documentation site's sandbox menu element
PUT /cdna-api/menus/element/53?properties={id,customFieldContent}
Content-Type: application/json

{"customFields": {"apiTestText": "Written by name"}}

# 200: apiTestText changed, apiTestNote and apiTestImage kept their values

{"customFields": {"apiTestTxt": "typo"}}

# 422 {"validationErrors": {"customFields.apiTestTxt": [{"message": "No custom field found for Menus\\Entities\\Element named apiTestTxt"}]}}

{"customFields": {"apiTestImage": 999999}}

# 422 {"validationErrors": {"image": [{"message": "Could not find the referenced record with id 999999"}]}}
JavaScript A product sync; the field names are this example's
// sync-fields.js: write only what changed (uses the coredna() helper from the OAuth how-to)
import { coredna } from './coredna.js';

// the other system's attribute -> the site's field, and how to convert the value
const FIELDS = {
  material:  { name: 'material',  convert: v => v.toLowerCase() },          // a select: its option key
  certified: { name: 'certified', convert: v => v === 'Y' },                // a switch
  careSheet: { name: 'careSheet', convert: v => v ? fileIds[v] : null },   // a file: its id, uploaded earlier
};
const fileIds = {};  // filled by the upload step: source file name -> id

function current(area) {                    // a customFieldContent entry -> the value a write takes
  if (!area) return null;
  if (area.category === 'image') return area.imageId ?? null;
  if (area.category === 'file') return area.fileId ?? null;
  if ('active' in area) return area.active;   // a switch
  return area.text ?? null;
}

export async function syncProduct(productId, source) {
  const { data } = await coredna('GET', 'products/product/' + productId, {
    query: { properties: '{id,customFieldContent}' },
  });
  const stored = data.customFieldContent ?? {};
  const changes = {};
  for (const [attribute, { name, convert }] of Object.entries(FIELDS)) {
    if (!(attribute in source)) continue;
    const value = convert(source[attribute]);
    if (String(current(stored[name]) ?? '') !== String(value ?? '')) changes[name] = value;
  }
  if (Object.keys(changes).length === 0) return 'unchanged';
  await coredna('PUT', 'products/product/' + productId, {
    query: { properties: '{id}' },
    body: { customFields: changes },
  });
  return Object.keys(changes);
}

Why this and not that

Not customFieldValues, the rows by id: on products, product categories and people, a list of rows replaces the record's custom fields, so a list that leaves one out deletes it; writing by name finds the rows itself and cannot remove a field. Not every field on every run: a write is live at once and has no revision to roll back to, so a sync that writes only what changed is also the one whose mistakes are smallest. Not the write's answer as proof: it can leave out fields that are still stored. And not a filter on field name and value together to find the record for a source key: the two conditions are matched separately, against any of the record's fields. Keep the source key in a field that nothing else can hold, or in your own mapping table.

Go deeper

Was this page helpful?

Your answer goes to the docs team.