Repository navigation
Store content types with their versions and the site record - #2455
Merged
Merged
Conversation
4 tasks done
pournasserian
added a commit
that referenced
this pull request
Oct 18, 2026
Closes #2461 ## What The third slice of F-SCH (#2415): entity types (SCH-03) and references to an entity or one of its fields (SCH-04), on top of the stored content types from #2451. There is no migration. - **Entity flag**: the definition document gains `"entity": true|false`, written first and read back as `ContentTypeDefinition.IsEntity`. A stored document without it (everything written by #2455) reads as a plain content type. - **Fact metadata**: `FactMetadata` sets the names every field of an entity type carries next to its `value`: `source` (`url`, `file`, `person` or `system`), `owner`, `validFrom`, `validUntil` and `lastVerified`. A field cannot opt out, because the definition has no syntax for it and an unknown field property such as `"fact": false` is already rejected. - **An entity type stays one**: `ContentType.Revise` rejects a definition that drops the flag (`entity_flag_removed`). A content type may still become an entity type. - **Field references**: `reference` takes an optional `field`, as in `{"target": "product", "field": "price"}`. `IFieldType.ReferencedTypes` becomes `ReferenceTargets` and returns `ReferenceTarget(Type, Field?)`. `list` passes on its item type's targets. - **Checks against stored data** (`ContentTypeDirectory`): - A field target must be a field of the target's current definition (`reference_field_unknown`). - The target must be an entity type (`reference_target_not_entity`). - A revision cannot remove a field another type references (`field_referenced`, naming the referencing type and field). - References to the type's own fields are checked against the definition being written. - **CONTRIBUTING.md**: the renamed member. ## Why SCH-03 and SCH-04, and the F-SCH acceptance: every field on an entity type carries source, owner and validity, a definition that drops them is rejected, and `ProductPage` can reference `Product.price`. Choices worth a look: - **The flag lives in the versioned definition, not a column.** Flagging a type is a schema change like any other, and SCH-05 will version it as one. Nothing queries the flag yet. A column can come with the first query that needs it. - **"A definition that drops them" is read as two rules.** - A field of an entity type has no way to opt out of the metadata. - An entity type cannot be revised into a plain content type, because that would strip provenance from every fact stored under it. If the maintainers want the flag to be reversible, for example through a reviewed change set once SCH-05 exists, only the check in `ContentType.Revise` changes. - **Field references only target entity types.** SCH-04 names entity fields, and only facts are worth pointing at field by field. Plain content types are referenced as whole entries, as before. - **`FactMetadata` is only names.** Checking a fact's value, whether it is unsourced or stale, and its owner comes with F-ENT (#2417). Pinning the names here keeps the schema document (SCH-06) and entity values in agreement. - **Removing a referenced field** is checked against every type's current definition in the site, and only when a revision removes fields. Tens of types per site keep that cheap (D26). Two concurrent schema writers could still slip past each other. Schema changes going through change sets (SCH-05) is where that gets serialised. - **`ReferencedTypes` is renamed to `ReferenceTargets`** on the field type extension point. It landed in #2455 and nothing outside this repository implements it. There is no versioning policy for extension interfaces yet (EXT-N2). ## Tests Unit: - The flag is written first and reads back. A document without it is a content type. A non-boolean flag and a field-level `"fact"` property are rejected. - `FactMetadata` pins its five properties and four source kinds. - An entity type rejects a revision that drops the flag, and a content type can take the flag. - `reference` accepts `field` and rejects a field that is malformed, blank or not a string, or one given without a target. It declares `ReferenceTarget("product", "price")`. - `list` passes a field target on. A definition reports field references. - The conformance base now checks that every declared target is a slug with an optional field name. All built-ins and both sample plugin types pass. Integration, each on SQLite and PostgreSQL: - An entity type stores `"entity": true` and reads back as one. A definition stored without the flag reads as a content type. - Dropping the flag fails and writes nothing, while flagging a content type works. - `product-page` references `product.price` directly and through a list. - Each of these fails with an error naming the field: a field the target lacks, a field of a non-entity type, and a revision removing `price` while two types reference it. Removing an unreferenced field works. - A type can reference its own field only as an entity type. - The reference scenario now defines `product`, `category`, `spec` and `region` as entity types and the four page types as content types, with `product-page` and `comparison-page` referencing `product.price`. All ran locally against a `postgres:17` container as well as SQLite. There is no migration and no pending model change, no new package and no new project reference. ## Checklist - [x] Linked to an issue a maintainer has confirmed - [x] Targets `v2` - [x] Tests or docs updated where behaviour changed - [x] No tool-generated co-author trailers or footers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2451
What
The second slice of F-SCH (#2415): content types are stored and read back through an application service. This builds on the Core model from #2445.
Siteholds the one well-known default site (SCH-07).IContentTypeDirectorylists types, gets the current version, gets a given version, creates and revises, each returning aResult. A revision names the version it starts from.ContentType.Restorerebuilds a stored type without validating it again, so a type stays readable after a field type it uses is gone.IFieldType.ReferencedTypes(empty by default) lets a field type declare the content types it points at.referencereturns its target, andlistasks its item type through the registry.Sitesis seeded with the default site by the migration.ContentTypesholds the site, name, slug and current version number, with(SiteId, Slug)unique.ContentTypeVersionsholds one row per version with the definition JSON,jsonbon PostgreSQL.(ContentTypeId, Version)is unique, and rows are inserted once and never changed.ContentTypeDirectorychecks slug uniqueness and reference targets against stored data. It writes throughIUnitOfWorkand appendscontent_type.createdorcontent_type.revisedthroughIAuditLog, with the content type as subject. The revised event'sPreviousVersionIdis the replaced version's row id.ContentTypesfor both providers, generated with the documented commands.Why
SCH-01 and SCH-07 need the types stored and scoped to a site. The epic's acceptance needs earlier versions to stay readable. AUD-N1 needs every schema write audited in the same transaction.
Some choices worth a look:
(SiteId, Slug)index, and the directory maps that unique violation to the sameslug_takenresult. A revision moves the version pointer withUPDATE … WHERE Version = @base, so the second writer updates no row and getsversion_conflict. The(ContentTypeId, Version)index backs this up. The mapping recognises SQLiteSQLITE_CONSTRAINT_UNIQUE/PRIMARYKEYand PostgreSQL23505.IFieldType.ReferencedTypes, not a special case for the built-inreferencetype. A plugin type that points at entries gets the same check (EXT-N1). This adds a defaulted member to the field type interface. Existing implementations are unaffected.TemporaryDatabaseandPostgresDataAttributefrom Add reading and JSON and CSV export of the audit log #2449 to run every case on both providers.Tests
Unit:
referencedeclares its target.listdeclares its item type's targets.Restorekeeps stored values without validating them.Integration, each on SQLite and PostgreSQL:
Result. Disabling the unique-violation mapping makes the create test fail withDbUpdateExceptionon both providers, and disabling the conditional update too makes the revision test fail, so both tests reach the database path.PostgreSQL only: the definition column is
jsonb. The existing migration test reverts the newest migration (ContentTypes) and reapplies it on SQLite.All of this ran locally against a
postgres:17container as well as SQLite. The schema tests passed five runs in a row.No new package and no new project reference.
Checklist
v2