Skip to content

Add field types and the content type model - #2448

Merged
pournasserian merged 1 commit into
v2from
2445-content-type-model
Oct 18, 2026
Merged

pournasserian merged 1 commit into
v2from
2445-content-type-model

Conversation

@pournasserian

Copy link
Copy Markdown
Member

Closes #2445

What

The first slice of F-SCH (#2415): the schema model in Core, with no storage yet.

  • Field type extension point (FluentCMS.Core.Schema): IFieldType is marked [ExtensionPoint]. It has a stable name and checks the settings a definition gives a field of its type, returning errors instead of throwing. FieldTypeRegistry resolves registered types by name and refuses to start with a duplicate or malformed name. FieldSettingsReader is the public helper the built-ins use: every setting a type reads counts as known, and anything else in the object comes back as setting_unknown with the list of settings the type does have.
  • Built-ins (FluentCMS.Core.Builtin.FieldTypes): the SCH-02 types text, richText (Markdown), number, boolean, dateTime, enum, media, reference (target type by slug), list and json. AddFluentCmsCore() discovers them with the same AddFluentCmsExtensions call a plugin assembly uses. list checks its item settings through the registry, so a list of a plugin type works like a list of a built-in. A list of lists is rejected, and the error points to json.
  • Definition document: ContentTypeDefinition holds an ordered list of FieldDefinition (name, type, required, settings) and stores as {"fields":[{"name":"price","type":"number","required":true,"settings":{"min":0}}]}. Parse checks only the shape and reports every problem with its JSON path; duplicate properties are rejected. Validate checks names (lower camel case or snake case, unique ignoring case), known types and each field's settings, and every error names its field.
  • Aggregate: ContentType has an id, site id (SCH-07), name, slug, version and current definition. Create reports every problem at once. Revise validates again and returns version n+1 with the same id, site, name and slug, leaving the current instance unchanged.
  • CONTRIBUTING.md: a paragraph on the field type contract and the conformance base.

Why

F-SCH is next on the critical path (#2432), and #2438 left field types to F-SCH as the first real extension point. The epic lists the aggregate and the field types as separate slices. This PR puts both in Core because the aggregate validates its definition through the field types. The next slice adds the application services, the stored type with its versions, the one site record, the checks that need stored data (slug unique per site, reference target exists), audit events, and migrations for both providers. Splitting there keeps this diff free of migrations.

Some choices worth a look:

  • Settings are a raw JSON object per field, and the field type interprets them. Typed settings classes would need polymorphic serialization that a plugin type could not join without touching Core.
  • The definition and field are classes, not records: JsonElement has no value equality, so record equality would be misleading.
  • A content type may have no fields, so an agent can create a type and add fields later.
  • A field type's name is stored in every definition that uses it, so the registry fails at startup on a duplicate or malformed name instead of at the first definition that uses it.

Tests

Conformance base FieldTypeConformanceTests (EXT-N1). Each type is registered once through the extension point under a valid name. It accepts the settings it documents and rejects invalid ones with a code and a message. It reports an unknown setting as setting_unknown. It never throws, whatever JSON value a setting holds. All ten built-ins pass it, and so does RatingFieldType, a sample type in the test assembly outside Builtin.

Unit: Core lists exactly the ten SCH-02 types, all as built-ins. Duplicate and malformed type names are rejected. A plugin type validates in a definition and inside a list, and is unknown without its assembly. A definition with one field of every type parses, validates, writes to JSON and reads back unchanged. The JSON shape is exact. Malformed JSON, the wrong root shape and every structural problem are reported with their path. Validation reports invalid and duplicate names, unknown types, bad settings, a list of lists and missing settings, each naming its field. The aggregate starts at version 1 scoped to its site and rejects an empty site, a blank or too long name, and invalid slugs. A revision makes the next version with the same identity, and an invalid one changes nothing. Field name and type name rules have their own tests.

ExtensionRegistrationTests has two adjustments now that Core has built-ins and the test assembly has a field type. The "no built-ins yet" test becomes "Core registers its built-ins through the catalog". The discovery test asserts that every catalogued contract is an extension point, instead of asserting it is IGreeting.

No new package and no new project reference. No model change, so no migration.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

@pournasserian pournasserian added this to the M1 Governance core milestone Oct 18, 2026
@pournasserian pournasserian added type:feature New capability v2 FluentCMS v2 (v2 branch) area:schema labels Oct 18, 2026
@pournasserian
pournasserian merged commit d9b311d into v2 Oct 18, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2445-content-type-model branch October 18, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New capability v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Content type model: field type extension point, built-in field types and the content type aggregate

1 participant