What happens to existing content when you edit a Collection's or Component's field definitions. Changing a schema in elek.io Core is not just a metadata edit - Core cascades the change into every affected Entry so stored content stays valid against its schema, all in a single commit.
For the field types this operates on, see fields.md. For the data model, see concepts.md.
Every field definition has a stable id (UUID). The cascade matches old and new definitions by id, never by slug.
- Keep a field's
idand you keep its data - even if you rename itsslugor change its type. - Give a field a new
id(even with the sameslug) and Core treats it as removing the old field and adding a new one - the old content is dropped.
When you call core.collections.update() or core.components.update(), always send back the existing field definitions with their original ids, changing only what you intend to change.
- Collection field definitions → every Entry in that Collection.
- Component field definitions → every Entry whose Collection has a
dynamicfield referencing that Component, transformed at the level of each Component item. References are followed transitively (a Component referenced by another Component is still reached), with cycle protection.
Core diffs old vs new definitions into three categories - added, removed, updated (any property differs) - and transforms each affected Entry accordingly.
| Change | Effect on existing Entries | Data loss? | Needs resolution? |
|---|---|---|---|
| Add an optional field | Value populated with the default (or null / empty per language) |
No | No |
| Add a required field with a default | Value populated with the default, replicated to every language | No | No |
| Add a required field, no default | - | No | Yes (missing_required) |
| Remove a field | Value is dropped from every Entry | Yes, permanent | No (silent) |
Rename a field's slug (same id) |
Value is moved to the new slug, content preserved | No | No |
| Change a field's type / constraints | Old value re-validated against the new schema, kept if it passes | Only if it can't pass | Yes if it fails (type_mismatch / constraint_violation) |
Turn a field unique (isUnique on, or add a slug field) |
Existing values scanned for cross-Entry duplicates per language | No | Yes if duplicates exist (unique_collision) |
Narrow ofComponents / ofCollections |
Component items / Entry references no longer allowed are stripped (per language) | Yes, permanent | No (silent) |
Renames, removals, additions with a usable default, and allowlist narrowing are unambiguous, so Core applies them without asking. Two of these discard content silently:
- Removing a field permanently drops its value from every Entry.
- Narrowing
ofComponentsorofCollectionsstrips items/references that are no longer allowed. An empty allowlist means "all" (nothing is stripped). Asset references are never stripped.
There is no separate confirmation step for these - the commit is made as part of the update. Treat field removal and allowlist narrowing as destructive operations.
A newly added field's value is built per its type:
- A configured
defaultValueis replicated to every Project language. - Optional
string/number/mdastfields getnullper language.referencefields get an empty array per language.dynamicfields get an empty array. boolean(toggle) fields always get a value (their default, orfalse) - they are never null.- A required field with no default cannot be auto-filled and raises a
missing_requiredissue (see below).
Some changes can't be resolved automatically - Core can't guess what a value should become. These raise issues and the update fails with a CoreError of type Conflict until you supply resolutions. The four issue types:
missing_required- a required field was added with no default.type_mismatch- a field'svalueTypechanged and an existing value doesn't fit the new type.constraint_violation- a constraint tightened (e.g.min/max, orisRequiredtoggled on) and an existing value no longer satisfies it.unique_collision- a field was made unique (or aslugfield was added) over a Collection that already holds the same value in more than one Entry for the same language. The first holder is kept and every other Entry is flagged.
The unresolved issues are attached as the cause of the thrown Conflict error - an array where each entry describes one problem:
{
entryId: '...', // the Entry that needs attention
collectionId: '...',
fieldDefinitionId: '...',
fieldSlug: 'price', // the Field, by its new slug
issue: 'type_mismatch', // why it can't be auto-resolved
currentValue: { /* ... */ }, // the existing value, if any
componentItemId: '...', // present for Component-item issues
transformedValues: { /* ... */ }, // the Entry's values after deterministic transforms
}A unique_collision issue is shaped a little differently. It sets transformedValues to {} and has no currentValue. Instead it carries value (the colliding string), language (the slot it collides in), and conflictingEntryId (the kept Entry that already holds the value). These are the same fields as the UniqueValueConflict thrown on a per-Entry create or update, so an editor renders both the same way.
Retry the update with a resolutions map keyed by Entry id, then field slug, to the corrected Value. Resolutions are type-checked against the new field's schema, and an invalid one throws BadRequest. For a unique_collision the corrected Value must be unique within the Collection for that language, since reusing the colliding value just fails the scan again.
import { CoreError } from '@elek-io/core';
const updatedDefinitions = /* the Collection's fieldDefinitions with your edits */;
try {
await core.collections.update({
...collection,
projectId: project.id,
fieldDefinitions: updatedDefinitions,
});
} catch (error) {
if (error instanceof CoreError && error.type === 'Conflict') {
const issues = error.cause; // array of issue objects shown above
// Build a corrected value for each issue
const resolutions = {};
for (const issue of issues) {
resolutions[issue.entryId] ??= {};
resolutions[issue.entryId][issue.fieldSlug] = /* a valid Value for the new schema */;
}
// Retry - deterministic transforms still apply, plus your resolutions
await core.collections.update({
...collection,
projectId: project.id,
fieldDefinitions: updatedDefinitions,
resolutions,
});
} else {
throw error;
}
}core.components.update() takes the same resolutions shape. For Component-item issues, use the componentItemId on each issue to target the right item.
The whole cascade is transactional. The Collection (or Component) file and every affected Entry are written, then committed once. The operation is wrapped in Core's git rollback - if anything fails, including an unresolved issue, the working tree is restored and nothing is committed. Every transformed and resolved value is re-validated against the new schema before it is written.
- No circular Components. Creating or updating a Component whose
dynamicfields reference back into itself (directly or transitively) throwsBadRequest. - No deleting a referenced Component.
core.components.delete()throwsConflictif any Collection or Component still references the Component. Remove the references first. - Collection / Component slug uniqueness is enforced per Project. Field-definition slugs need not be globally unique - they are matched by
id, which is what makes renames safe.
fields.md- field definition shapes and constraints-error-handling.md-CoreError,Conflict, andwithGitRollbackusage.md- creating and updating Collections, Components and Entries