diff --git a/pkg/data/diff.go b/pkg/data/diff.go new file mode 100644 index 000000000..9b9efc4db --- /dev/null +++ b/pkg/data/diff.go @@ -0,0 +1,61 @@ +package data + +type ( + modelDiffType string + + // ModelDiff defines one identified missmatch between two models + ModelDiff struct { + Type modelDiffType + Original *Attribute + Asserted *Attribute + } + + ModelDiffSet []*ModelDiff +) + +const ( + AttributeMissing modelDiffType = "attributeMissing" + AttributeTypeMissmatch modelDiffType = "typeMissmatch" +) + +// Diff calculates the diff between models a and b where a is used as base +func (a *Model) Diff(b *Model) (out ModelDiffSet) { + bIndex := make(map[string]struct { + found bool + attr *Attribute + }) + for _, _attr := range b.Attributes { + attr := _attr + bIndex[attr.Ident] = struct { + found bool + attr *Attribute + }{ + attr: attr, + } + } + + for _, _attrA := range a.Attributes { + attrA := _attrA + + // Missmatches + attrBAux, ok := bIndex[attrA.Ident] + if !ok { + out = append(out, &ModelDiff{ + Type: AttributeMissing, + Original: attrA, + }) + continue + } + + // Typecheck + if attrA.Type.Type() != attrBAux.attr.Type.Type() { + out = append(out, &ModelDiff{ + Type: AttributeTypeMissmatch, + Original: attrA, + Asserted: attrBAux.attr, + }) + } + } + + return +} diff --git a/pkg/data/model.go b/pkg/data/model.go new file mode 100644 index 000000000..eb89fa013 --- /dev/null +++ b/pkg/data/model.go @@ -0,0 +1,270 @@ +package data + +import ( + "fmt" + + "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/pkg/minions" +) + +type ( + // Model describes the underlying data and its shape + Model struct { + StoreID uint64 + Ident string + + ResourceID uint64 + ResourceType string + + Attributes AttributeSet + } + ModelSet []*Model + + // Attribute describes a specific value of the dataset + Attribute struct { + Ident string + + MultiValue bool + + // Store describes the strategy the underlying storage system should + // apply to the underlying value + Store StoreStrategy + // Type describes what the value represents and how it should be + // encoded/decoded + Type attributeType + } + AttributeSet []*Attribute + + AttributeType string + attributeType interface { + Type() AttributeType + } + + // TypeID handles ID (uint64) coding + // + // Encoding/decoding might be different depending on + // 1) underlying store (and dialect) + // 2) value codec (raw, json ...) + TypeID struct { + // @todo need to figure out how to support when IDs + // generated/provided by store (SERIAL/AUTOINCREMENT) + GeneratedByStore bool + } + + // TypeRef handles ID (uint64) coding + reference info + // + // Encoding/decoding might be different depending on + // 1) underlying store (and dialect) + // 2) value codec (raw, json ...) + TypeRef struct { + RefModel *Model + RefAttribute *Attribute + } + + // TypeTimestamp handles timestamp coding + // + // Encoding/decoding might be different depending on + // 1) underlying store (and dialect) + // 2) value codec (raw, json ...) + TypeTimestamp struct{} + + // TypeTime handles time coding + // + // Encoding/decoding might be different depending on + // 1) underlying store (and dialect) + // 2) value codec (raw, json ...) + TypeTime struct{} + + // TypeDate handles date coding + // + // Encoding/decoding might be different depending on + // 1) underlying store (and dialect) + // 2) value codec (raw, json ...) + TypeDate struct{} + + // TypeNumber handles number coding + // + // Encoding/decoding might be different depending on + // 1) underlying store (and dialect) + // 2) value codec (raw, json ...) + TypeNumber struct { + Precision uint + Scale uint + } + + // TypeText handles string coding + // + // Encoding/decoding might be different depending on + // 1) underlying store (and dialect) + // 2) value codec (raw, json ...) + TypeText struct{ Length uint } + + // TypeBoolean + TypeBoolean struct{} + + // TypeEnum + TypeEnum struct { + Values []string + } + + // TypeGeometry + TypeGeometry struct{} + + // TypeJSON handles coding of arbitrary data into JSON structure + // NOT TO BE CONFUSED with encodedField + // + // Encoding/decoding might be different depending on + // 1) underlying store (and dialect) + // 2) value codec (raw, json ...) + TypeJSON struct{} + + // TypeBlob store/return data as + TypeBlob struct{} + + TypeUUID struct{} +) + +const ( + typeID AttributeType = "id" + typeRef AttributeType = "ref" + typeTimestamp AttributeType = "timestamp" + typeTime AttributeType = "time" + typeDate AttributeType = "date" + typeNumber AttributeType = "number" + typeText AttributeType = "text" + typeBoolean AttributeType = "boolean" + typeEnum AttributeType = "enum" + typeGeometry AttributeType = "geometry" + typeJSON AttributeType = "json" + typeBlob AttributeType = "blob" + typeUUID AttributeType = "uuid" +) + +// FindByIdent returns the model that matches the ident +func (mm ModelSet) FindByIdent(ident string) *Model { + for _, m := range mm { + if m.Ident == ident { + return m + } + } + + return nil +} + +// FilterByReferenced returns all of the models that reference b +func (aa ModelSet) FilterByReferenced(b *Model) (out ModelSet) { + for _, aModel := range aa { + if aModel.Ident == b.Ident { + continue + } + + for _, aAttribute := range aModel.Attributes { + switch casted := aAttribute.Type.(type) { + case TypeRef: + if casted.RefModel.Ident == b.Ident { + out = append(out, aModel) + } + } + } + } + + return +} + +// HasAttribute returns true when the model includes the specified ident +func (m Model) HasAttribute(ident string) bool { + return m.Attributes.FindByIdent(ident) != nil +} + +func (aa AttributeSet) FindByIdent(ident string) *Attribute { + for _, a := range aa { + if a.Ident == ident { + return a + } + } + + return nil +} + +// Validate performs a base model validation before it is passed down +func (m Model) Validate() error { + if m.Ident == "" { + return fmt.Errorf("ident not defined") + } + + seen := make(map[string]bool) + for _, attr := range m.Attributes { + if attr.Ident == "" { + return fmt.Errorf("invalid attribute ident: ident must not be empty") + } + + if !handle.IsValid(attr.Ident) { + return fmt.Errorf("invalid attribute ident: %s is not a valid handle", attr.Ident) + } + + if seen[attr.Ident] { + return fmt.Errorf("invalid attribute %s: duplicate attributes are not allowed", attr.Ident) + } + seen[attr.Ident] = true + + if minions.IsNil(attr.Type) { + return fmt.Errorf("attribute does not define a type: %s", attr.Ident) + } + } + + return nil +} + +// Receivers to conform to the interface + +func (t TypeID) Type() AttributeType { + return typeID +} + +func (t TypeRef) Type() AttributeType { + return typeRef +} + +func (t TypeTimestamp) Type() AttributeType { + return typeTimestamp +} + +func (t TypeTime) Type() AttributeType { + return typeTime +} + +func (t TypeDate) Type() AttributeType { + return typeDate +} + +func (t TypeNumber) Type() AttributeType { + return typeNumber +} + +func (t TypeText) Type() AttributeType { + return typeText +} + +func (t TypeBoolean) Type() AttributeType { + return typeBoolean +} + +func (t TypeEnum) Type() AttributeType { + return typeEnum +} + +func (t TypeGeometry) Type() AttributeType { + return typeGeometry +} + +func (t TypeJSON) Type() AttributeType { + return typeJSON +} + +func (t TypeBlob) Type() AttributeType { + return typeBlob +} + +func (t TypeUUID) Type() AttributeType { + return typeUUID +} diff --git a/pkg/data/store_codec.go b/pkg/data/store_codec.go new file mode 100644 index 000000000..3ddc09a5b --- /dev/null +++ b/pkg/data/store_codec.go @@ -0,0 +1,33 @@ +package data + +type ( + // StoreStrategy defines how values for a specific model attribute + // are retrieved or stored + // + // If attribute does not have store strategy set + // store driver should use attribute name to determinate + // source/destination of the value (table column, json doc property) + StoreStrategy any + + // StoreCodecJSON defines that values are encoded/decoded into + // a JSON document (stored under ident) + // Path defines exact location inside the doc. + StoreCodecJSON struct { + Ident string + Path []any + } + + // { "@value": ... "@type": .... } + // StoreCodecJSONLD struct { Ident string; Path []any } + + // StoreCodecXML + //StoreCodecXML struct {} + + // StoreCodecAlias defines case when value is not stored + // under the same column (in case of an SQL table) + // + // Example: attribute ident is "foo" and database table column is "bar" + StoreCodecAlias struct { + Ident string + } +)