← Back to Library

Data Model Reference

concept ·  modelers, app devs
data-modeltemplatesdocumentsidentity

Data Model Reference

In one sentence. WIP's data model is four building blocks — terminologies/terms (controlled vocabulary), templates (versioned document schemas), documents (validated, identity-addressed instances of a template), and files (binary attachments) — unified by the Registry's canonical IDs and synonym resolution (wip://data-model).

Why it exists. WIP is one generic, template-driven platform, not a bespoke table per app. A template "defines the structure, validation rules, and constraints that documents must conform to" and "support[s] inheritance, allowing child templates to extend parent templates" (template.py, Template docstring). A document "support[s] versioning through identity-based upsert logic," using the identity hash "to detect when a document should be updated (new version) rather than created as a new entity" (document.py, Document docstring). Together, templates + identity give every document class real validation and safe schema evolution (multiple template versions can stay active at once) without every caller inventing its own dedup key.

The model

Terminologies & terms

A terminology is a namespaced controlled vocabulary; a term is one entry in it. Documents store both the value the caller submitted and the resolved term_id (wip://data-model).

Terminology fieldNotes
terminology_id, namespace, value, label, descriptionidentity/display
case_sensitive, allow_multiple, extensible, mutablebooleans, default false; mutable=true implies extensible=true
metadata{source, source_url, version, language(default "en"), custom}
status, term_countlifecycle / derived count

(schemas/def-store.jsonTerminologyResponse, CreateTerminologyRequest, TerminologyMetadata)

Term fieldNotes
term_id, namespace, terminology_id, valueidentity
aliases[]alternative values that resolve to this term (e.g. ["MR.", "mr"])
label, description, sort_order, parent_term_iddisplay / hierarchy
translations[]{language (ISO 639-1), label, description}
status, deprecated_reason, replaced_by_term_idlifecycle

(schemas/def-store.jsonTermResponse, CreateTermRequest, TermTranslation)

Terms carry typed ontology relations to each other: is_a, part_of, has_part, regulates, positively_regulates, negatively_regulates, each {source_term_id, target_term_id, relation_type}. Relations support ancestor/descendant/parent/child traversal and bulk import from OBO Graph JSON (wip://data-model; schemas/def-store.jsonCreateTermRelationRequest, TraversalResponse).

Templates & fields

Template fieldNotes
template_id, namespace, value, label, descriptionidentity/display; value unique within namespace
versionincremented on update — see How it works
extends, extends_versionparent template + its pinned version
identity_fields[]composite key that makes two documents "the same"; [] = append-only
header_fields[]fields shown in compact peer/header projections (see Gotchas)
usageentity (default) | reference (reserved, currently behaves like entity) | relationship. Immutable after creation.
source_templates[], target_templates[]relationship-only allow-lists for the edge's two endpoints
versionedtrue (default, updates create new versions) | false (updates overwrite in place). Immutable after creation, and requires non-empty identity_fields
fields[]FieldDefinition list
rules[]ValidationRule list, cross-field
metadata{domain, category, tags[], custom}
reportingPostgreSQL sync config — see below
statusdraft | active | inactive

(template.py; schemas/template-store.jsonTemplateResponse, CreateTemplateRequest)

FieldDefinition — one entry per field:

PropertyNotes
name, label, typetypestring, number, integer, boolean, date, datetime, term, reference, file, object, array
mandatory, default_value
terminology_reftype=term (legacy — prefer reference)
template_ref + template_ref_versiontype=object; the pinned version is mandatory whenever template_ref is set — nested data validates against exactly that (template_id, version) pair, never "latest"
reference_type, target_templates[], include_subtypes, target_terminologies[], version_strategytype=reference; reference_typedocument, term, terminology, template; version_strategylatest, pinned; for an array of references the same slots live on the field itself and reference_type is mandatory
file_configtype=file: {allowed_types[] (MIME patterns), max_size_mb (≤100), multiple, max_files}
array_item_type, array_terminology_ref, array_template_ref (+ array_template_ref_version), array_file_configtype=array
validation{pattern, min_length, max_length, minimum, maximum, enum}
semantic_typeemail, url (string) · latitude, longitude, percentage (number) · duration, geo_point (object)
full_text_indexedstring-only; builds a Postgres tsvector + GIN column for the reporting search endpoint; requires reporting.sync_enabled=true
inherited, inherited_frompopulated at inheritance-resolution time, not stored

(field.py; schemas/template-store.jsonFieldDefinition, FieldType, ReferenceType, VersionStrategy, SemanticType, FileFieldConfig)

ValidationRule — cross-field rule:

PropertyNotes
typeconditional_required, conditional_value, mutual_exclusion, dependency, pattern, range
conditions[]{field, operator (equals/not_equals/in/not_in/exists/not_exists), value}
target_field / target_fields[]single target, or the set for mutual_exclusion
required, allowed_values[], pattern, minimum, maximum, error_messageper-type payload

(rule.py; schemas/template-store.jsonValidationRule, RuleType, Condition)

reporting (PostgreSQL sync config attached to a template): sync_enabled (default true), sync_strategy (latest_only upsert | all_versions insert-all), table_name (auto-generated from value if unset), include_metadata, flatten_arrays, max_array_elements (1–100, default 10) (template.pyReportingConfig). The reporting sync's own service-side models (jobs, alerts, search) are a separate surface — see Reporting & Analytics.

Documents

Document fieldNotes
document_id, namespacedocument_id from the Registry (UUID7, time-ordered)
template_id, template_version, template_valuewhich template (and exact version) validated this document
identity_hashSHA-256 over the resolved identity_fields values; "" when the template has no identity_fields — see the ⚠ GAP note under Rules & invariants
versionper-identity version counter (see How it works)
datathe document content, validated against the template's fields
term_references[], references[], file_references[]resolved term/reference/file fields, one entry per resolved field
statusactive | inactive | archived — see Gotchas
is_latest_version, latest_versionwhether this row is the newest version, and what that version number is
metadata{source_system, warnings[], custom}
created_at/by, updated_at/by

(document.py; schemas/document-store.jsonDocumentResponse, DocumentCreateRequest, DocumentStatus, DocumentMetadata)

Two document-derived projections exist for consumers that don't want the full document shape:

Files

A file is a first-class Registry-identified entity stored in MinIO and referenced by documents through file-typed fields, just like any other reference type (document_store/models/file.py). Fields: file_id (UUID7), namespace, filename, content_type, size_bytes, statusorphan (uploaded, not yet referenced by an active document) | active | inactive, metadata {description, tags[], category, custom}.

Registry, namespaces & synonyms (shape only)

These entities live behind components/registry/src, outside this doc's source; the shapes below come from the wire schema only. Behavior belongs to Identity & the Registry and Namespaces & Tenancy.

Namespace fieldNotes
prefix, descriptione.g. dev, staging, prod
isolation_modeopen (cross-namespace refs allowed) | strict (same-namespace only)
allowed_external_refs[]optional allowlist of external namespace prefixes (open mode)
id_configper-entity-type ID algorithm config; defaults to UUID7 for all
deletion_moderetain (soft-delete only, default) | full (allows hard-delete)
status

(schemas/registry.jsonNamespaceResponse, NamespaceCreate)

Registry entry fieldNotes
entry_id, namespace, entity_typeentity_typeterminologies, terms, templates, documents, files
primary_composite_key, primary_composite_key_hashthe entry's primary composite key and its hash
synonyms[]{namespace, entity_type, composite_key, composite_key_hash, source_info {system_id, endpoint_url}, created_at, created_by} — alternate keys that resolve to the same entry
search_values[], metadata, status

(schemas/registry.jsonEntryDetailResponse, Synonym, SourceInfo)

Every entity in the model above (terminology, term, template, document, file) is Registry-identified: the Registry mints the canonical ID and any registered synonyms resolve to it in O(1) (wip://data-model → "Registry & Synonyms").

How it works

Document write (document_store/services/document_service.py):

  1. ValidationService validates data against the template's resolved fields, producing term_references / references / file_references arrays for every term/reference/file-typed field.
  2. The template's identity_fields are extracted into identity_values; for a bulk write, identity_values is sent to the Registry, which mints or resolves document_id and returns identity_hash (DocumentService.bulk_create, Stage 2). A dry-run validate (no Registry call) computes the SHA-256 hash locally instead (_result_to_validation_response).
  3. Document-store looks for an existing active document at that document_id:
    • none found → insert version 1.
    • found, but data + the three reference arrays are byte-identical to the current version → no-op: returns the existing version, is_new=false, no new version, no event (_data_has_changed).
    • found, data changed, template versioned=true (default) → deactivate the current version (status → inactive) and insert version+1 with the same document_id (_create_new_version).
    • found, data changed, template versioned=false → overwrite the existing row in place: same document_id, same version, fresh data/timestamps, no history kept (_overwrite_in_place).
  4. A template with empty identity_fields is append-only: every POST is a new, standalone document (nothing to match against), and PATCH is rejected outright with error code append_only.
  5. PATCH applies an RFC 7396 JSON Merge Patch to the current data, re-validates against the same template_version the document was created with (not the latest active version — see Gotchas), rejects any attempt to change an identity_fields value (identity_field_change — create a new document with POST instead), and supports if_match for serialized concurrency (bulk_patch apply loop).

Template write (template_store/services/template_service.py):

  1. create_template enforces relationship-template shape when usage="relationship": non-empty source_templates/target_templates, and matching source_ref/target_ref fields of reference_type=document whose own target_templates equal the template-level lists (_validate_relationship_template_shape). For any other usage, source_templates/target_templates must be empty.
  2. update_template never edits a template in place — it inserts a new template document with the same template_id and version = (max existing version for this value) + 1. Old versions are not deactivated, so several template versions can be active at once, letting existing documents keep validating against the version they were created with while new documents target the latest (update_template docstring: "Creates a NEW template document with incremented version... allows multiple versions to exist simultaneously"). If nothing changed (_template_has_changed), no new version is written.
  3. usage, versioned, source_templates, target_templates are carried over unchanged from the original on every update — they are immutable after the template's first version.
  4. Draft mode: create with status="draft" to skip reference validation (enables circular or order-independent creation of interdependent templates). POST /templates/{id}/activate validates the whole reachable set of draft references as one unit and activates them all together — any failure activates none (activate_template).
  5. Inheritance (InheritanceService.resolve_template): a template with extends must also set extends_version — there is no "resolve to latest parent" fallback (a stored template without it is a data-integrity error, not a cue to float). Fields merge child-over-parent by name, each tagged inherited/inherited_from; rules concatenate with no dedup; identity_fields resolve from the nearest ancestor (walking child→root) that declares any.
  6. Delete: soft-delete sets the version to inactive; hard-delete additionally requires the owning namespace's deletion_mode="full". Either is blocked while any other template still extends this one (delete_template).

Rules & invariants

Gotchas

See also

Generated from real source code, not hand-written — see the WIP Technical Library.