Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/typed-fields-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cloudhal/core-contracts": minor
"@cloudhal/core-ts": minor
---

Typed custom fields, step 1 (RFC-0041 / ADR-0043). `Site.collections.<key>.fields` takes an ordered list of 1–30 field definitions in nine types (`text`, `richText`, `number`, `boolean`, `date`, `datetime`, `url`, `image`, `select`), discriminated by `type`, each with per-locale labels, `localized` and (except `boolean`) `requiredToPublish`. The new `@cloudhal/core-ts/fields` subpath (Worker-safe, eval-free) checks definitions (reserved keys, unique keys and options, 30 per collection, 200 per site, 256 KB of collection settings, the plain-edit guard), checks values through ADR-0030's `validateProps` (only changed values; unknown keys refused once a collection has definitions; 256 KB per entry), checks `requiredToPublish`, emits the JSON Schema agents receive (strict-mode safe), and resolves values across translations (`resolveFields`). Nothing reads it yet: the store, API, MCP, Studio and renderer adopt it in later steps, so nothing changes for existing sites.
2 changes: 1 addition & 1 deletion crates/cloudhal-schema/src/generated/content_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ pub struct ContentEntry {
pub cover_image: ::std::option::Option<ImageRef>,
#[serde(rename = "createdAt")]
pub created_at: Timestamp,
#[doc = "Collection-specific values; any JSON."]
#[doc = "Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections.<key>.fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value."]
pub fields: ::serde_json::Map<::std::string::String, ::serde_json::Value>,
pub id: Id,
pub locale: Locale,
Expand Down
519 changes: 519 additions & 0 deletions crates/cloudhal-schema/src/generated/site.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"reason": "a boolean field cannot be required to publish (absent reads as false)",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "online",
"type": "boolean",
"labels": {
"en": {
"label": "Online"
}
},
"localized": false,
"requiredToPublish": true
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"reason": "only text and rich text fields are searchable",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "deadline",
"type": "date",
"labels": {
"en": {
"label": "Deadline"
}
},
"localized": false,
"searchable": true
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"reason": "help has at most 300 characters",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "venue",
"type": "text",
"labels": {
"en": {
"label": "Venue",
"help": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
},
"localized": true
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"reason": "a field key is lower camelCase",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "start-date",
"type": "text",
"labels": {
"en": {
"label": "Venue"
}
},
"localized": true
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"reason": "a field key has at most 40 characters",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"type": "text",
"labels": {
"en": {
"label": "Venue"
}
},
"localized": true
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"reason": "a field key starts with a lower-case letter",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "Venue",
"type": "text",
"labels": {
"en": {
"label": "Venue"
}
},
"localized": true
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"reason": "labels are keyed by BCP 47 locale",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "venue",
"type": "text",
"labels": {
"English": {
"label": "Venue"
}
},
"localized": true
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"reason": "a label is not empty",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "venue",
"type": "text",
"labels": {
"en": {
"label": ""
}
},
"localized": true
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"reason": "a label has at most 100 characters",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "venue",
"type": "text",
"labels": {
"en": {
"label": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
},
"localized": true
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"reason": "labels name at least one locale",
"data": {
"schemaVersion": 1,
"id": "site_01HZX8K3Q9T1",
"projectId": "proj_01HZX8K3Q9AB",
"name": "Minimal",
"slug": "minimal",
"defaultLocale": "en",
"locales": ["en", "tr"],
"domains": [],
"pageIds": [],
"theme": {
"name": "blog"
},
"rev": 0,
"createdAt": "2026-10-23T20:00:00Z",
"updatedAt": "2026-10-23T20:00:00Z",
"collections": {
"events": {
"fields": [
{
"key": "venue",
"type": "text",
"labels": {},
"localized": true
}
]
}
}
}
}
Loading
Loading