Documentation
¶
Index ¶
- func NewPersistentRecordTransformer(registry forma.SchemaRegistry) model.PersistentRecordTransformer
- func NewTransformer(registry forma.SchemaRegistry) *transformer
- func NormalizeDottedKeys(data map[string]any, cache forma.SchemaAttributeCache, ...) map[string]any
- func ToFloat64(v any) (float64, bool)
- type AttributeConverter
- func (c *AttributeConverter) FromEAVRecord(record model.EAVRecord, valueType forma.ValueType) (model.EntityAttribute, error)
- func (c *AttributeConverter) FromEAVRecords(records []model.EAVRecord) ([]model.EntityAttribute, error)
- func (c *AttributeConverter) SetRelationRoots(lookup RelationRootsLookup)
- func (c *AttributeConverter) ToEAVRecord(attr model.EntityAttribute, rowID uuid.UUID) (model.EAVRecord, error)
- func (c *AttributeConverter) ToEAVRecords(attributes []model.EntityAttribute, rowID uuid.UUID) ([]model.EAVRecord, error)
- type RelationRoots
- type RelationRootsAware
- type RelationRootsLookup
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewPersistentRecordTransformer ¶
func NewPersistentRecordTransformer(registry forma.SchemaRegistry) model.PersistentRecordTransformer
NewPersistentRecordTransformer creates a new PersistentRecordTransformer instance
func NewTransformer ¶
func NewTransformer(registry forma.SchemaRegistry) *transformer
NewTransformer creates a new Transformer instance backed by the provided schema registry.
func NormalizeDottedKeys ¶
func NormalizeDottedKeys( data map[string]any, cache forma.SchemaAttributeCache, arrays schemavalidate.ArrayPaths, ) map[string]any
NormalizeDottedKeys rewrites literal dotted keys into their nested paths, producing the document handed to the schema validator.
It is for the validator only. The writer keeps receiving the caller's original map, so flattenToAttributes and #312's dedupe remain the single authority on which record wins: records carry the spelling that produced them, and the last spelling replaces the whole logical attribute. Merging spellings into one document destroys those tags, so a normalized document cannot re-derive that precedence in every case — which is why it must never reach the write path. Nothing here can change which records are written.
What it is for: attribute names in this codebase are dotted, so a caller reading the metadata may address contact.email either as a nested object or as a literal key. Left literal, JSON Schema treats it as an unknown property — with no additionalProperties declared it passes validation and its value is never examined, making validation trivially bypassable (#314). Expanding it first means the value is checked like any other.
Interior paths are expanded too, not just leaf attributes: {"contact.snapshot": {"code": …}} hides "code" from validation exactly the way a literal leaf key hides its own value. The test is isKnownAttributeOrParent — the same predicate flattenToAttributes uses — so a name is expanded when the schema defines it or defines something beneath it. An unknown dotted key is left alone; the writer still rejects it with "attribute is not defined".
A dotted key is not expanded when the schema declares an array between the key's own position and its leaf, because it cannot be: nesting requirement.areas.city at the top level puts an object where the schema declares an array, and the caller gets a 400 naming a type they never sent. Such a value is therefore not validated at all — the documented limit of this function.
The discriminator is where the caller writes the key, not which attribute it names. The same name is expanded and validated when it is written inside an element of the array it lies under: within an element of requirement.areas the array is already behind us, so "city" lands in the right place, and a dotted "snapshot.code" inside a propertyInterests element nests legally. That is ArrayPaths.CrossesBelow asking the question relative to the current node. It holds unconditionally on the shipped schemas only because none of them nests an array inside an array.
Relation roots need no rule here, because the write paths hand this function a payload RelationIndex.StripComputedFields has already emptied of them: the strip deletes the whole relation subtree — the root and every dotted descendant such as contactSnapshot.name (#318) — so no name beneath a relation root is left to expand. Pinned by TestStripLeavesNothingCoveredForTheValidator.
That is a statement about those callers, not a guarantee this package can make. Each strip site is guarded on the caller holding a relation index (entity_crud_service.go, entity_batch_service.go), and a caller that skips the strip reaches this function with contactSnapshot.name still present, where the ordinary rule expands it like any other known dotted name. That expansion is what #314 carved out against, and the carve-out is gone (#318).
arrays comes from schemavalidate, the only place that knows which paths are arrays; the metadata cache records requirement.areas.city and contact.email identically. Passing a nil set is safe and means "nothing known to be an array".
When both spellings are present the literal wins, matching encoding/json's duplicate-key semantics. Keys are applied in sorted order, and for any dotted name X.Y the shorter spelling X sorts before X.Y, so the longer, more specific one is applied last.
That is not the writer's rule, and this document does not claim to reproduce it. The writer's dedupe drops the losing spelling's records for the whole logical attribute; here the losing spelling's *siblings* are merged in, which is what keeps every persisted value visible to the validator. At an array the two rules diverge outright — see mergeSlices for the false rejection that follows and why it is accepted.
The input is never mutated: maps and slices are rebuilt rather than shared.
Types ¶
type AttributeConverter ¶
type AttributeConverter struct {
// contains filtered or unexported fields
}
AttributeConverter provides conversion between model.EntityAttribute and model.EAVRecord
func NewAttributeConverter ¶
func NewAttributeConverter(registry forma.SchemaRegistry) *AttributeConverter
NewAttributeConverter creates a new AttributeConverter instance
func (*AttributeConverter) FromEAVRecord ¶
func (c *AttributeConverter) FromEAVRecord(record model.EAVRecord, valueType forma.ValueType) (model.EntityAttribute, error)
FromEAVRecord converts an model.EAVRecord to an model.EntityAttribute
func (*AttributeConverter) FromEAVRecords ¶
func (c *AttributeConverter) FromEAVRecords(records []model.EAVRecord) ([]model.EntityAttribute, error)
FromEAVRecords converts a slice of EAVRecords to EntityAttributes
func (*AttributeConverter) SetRelationRoots ¶
func (c *AttributeConverter) SetRelationRoots(lookup RelationRootsLookup)
SetRelationRoots installs the relation-root lookup consulted by the required-policy check in FromEAVRecords. A nil lookup, or one that answers an empty set, leaves enforcement exactly as it was.
func (*AttributeConverter) ToEAVRecord ¶
func (c *AttributeConverter) ToEAVRecord(attr model.EntityAttribute, rowID uuid.UUID) (model.EAVRecord, error)
ToEAVRecord converts an model.EntityAttribute to an model.EAVRecord.
Its conversion failures are plain errors; the numeric and bool cases name the attribute by AttrID, the rest take their identity from ToEAVRecords' wrap. AttrID is the EAV key and the only identity this layer holds — the attribute name would need a metadata cache it does not take. The caller-facing carrier that names the attribute and wraps forma.ErrInvalidInput is raised earlier, at populateTypedValue (typed_value.go); reaching an error here on the write path means the value got past it, so it is operator-visible by design.
func (*AttributeConverter) ToEAVRecords ¶
func (c *AttributeConverter) ToEAVRecords(attributes []model.EntityAttribute, rowID uuid.UUID) ([]model.EAVRecord, error)
ToEAVRecords converts a slice of EntityAttributes to EAVRecords
type RelationRoots ¶
type RelationRoots map[string]struct{}
RelationRoots is the set of top-level property names a schema derives from a parent entity, i.e. the properties carrying `x-relation`.
It is declared here, as a plain set, rather than by importing the relation index: the index lives in package internal, which already depends on this package. The set's element type is unnamed, so internal can build one and assign it without a conversion.
The payload strip does not consult this set. RelationIndex.StripComputedFields applies its own subtree predicate (RelationIndex.coversRelationSubtree) to drop the whole relation subtree before validation, because it is derived on read and never persisted.
What consults this set is one check: the required-policy check in AttributeConverter.FromEAVRecords, which skips policies beneath a relation root (#315). That check is not read-only — ToAttributes runs FromEAVRecords on every create and update (transformer.go), and FromPersistentRecord runs it on read — so a transformer that only ever serves writes still needs the set installed. The write path's other required check, validateRequiredAttributesFromInput (transformer.go), has no such carve-out and never consults this set.
func (RelationRoots) Covers ¶
func (r RelationRoots) Covers(name string) bool
Covers reports whether name lies strictly beneath a relation root.
The question is asked about the absolute attribute name, with no positional prefix. Relation roots are top-level properties of the entity schema, and the names asked about are the metadata cache's absolute attribute names, so "beneath a relation root" is a property of the name alone.
A name that *is* a relation root is not covered: this reports names strictly beneath one, so the root's own required policy stays enforced — pinned by TestFromEAVRecordsEnforcesRequiredPolicyOnRelationRootItself. The write path's strip predicate (RelationIndex.coversRelationSubtree) deliberately differs there and matches the bare root as well, because the root is the nested spelling the strip removes.
type RelationRootsAware ¶
type RelationRootsAware interface {
SetRelationRoots(lookup RelationRootsLookup)
}
RelationRootsAware is implemented by the transformers in this package so the relation roots can be installed after construction.
NewEntityManager resolves the relation index — from the registry, or from the one its caller hands in — after the transformer it was given already exists, so the lookup is installed once at wiring time rather than injected at construction. Install before the transformer is used concurrently; nothing reads the field until then.
type RelationRootsLookup ¶
type RelationRootsLookup func(schemaName string) RelationRoots
RelationRootsLookup answers a schema's relation roots by schema name.
It exists so package internal — which owns RelationIndex and is the only place that can build a RelationRoots set — can hand the set to this package without an import cycle, at the seams that only know a schema ID.