package dal import ( "encoding/json" "fmt" "strings" "github.com/cortezaproject/corteza/server/pkg/handle" "github.com/modern-go/reflect2" ) type ( // ModelRef is used to retrieve a model from the DAL based on given params ModelRef struct { ConnectionID uint64 ResourceID uint64 ResourceType string Resource string Refs map[string]any } // Model describes the underlying data and its shape Model struct { ConnectionID uint64 `json:"connectionID"` Ident string `json:"ident"` Label string `json:"label"` Resource string `json:"resource"` ResourceID uint64 `json:"resourceID"` ResourceType string `json:"resourceType"` // Refs is an arbitrary map to identify a model // @todo consider reworking this; I'm not the biggest fan Refs map[string]any `json:"refs"` SensitivityLevelID uint64 `json:"sensitivityLevelID"` Attributes AttributeSet `json:"attributes"` Constraints map[string][]any `json:"constraints"` Indexes IndexSet `json:"indexes"` } ModelSet []*Model // Attribute describes a specific value of the dataset Attribute struct { Ident string `json:"ident"` Label string `json:"label"` SensitivityLevelID uint64 `json:"sensitivityLevelID"` MultiValue bool `json:"multiValue"` PrimaryKey bool `json:"primaryKey"` // If attribute has SoftDeleteFlag=true we use it // when filtering out deleted items SoftDeleteFlag bool `json:"softDeleteFlag"` // System indicates the attribute was defined by the system System bool `json:"system"` // Is attribute sortable? // Note: all primary keys are sortable Sortable bool `json:"sortable"` // Can attribute be used in query expression? Filterable bool `json:"filterable"` // Store describes the strategy the underlying storage system should // apply to the underlying value Store Codec `json:"store"` // Type describes what the value represents and how it should be // encoded/decoded Type Type `json:"type"` } // auxAttribute is a helper struct used for marshaling/unmarshaling // // This is required since some fields are interfaces auxAttribute struct { Ident string `json:"ident"` Label string `json:"label"` SensitivityLevelID uint64 `json:"sensitivityLevelID"` MultiValue bool `json:"multiValue"` PrimaryKey bool `json:"primaryKey"` SoftDeleteFlag bool `json:"softDeleteFlag"` System bool `json:"system"` Sortable bool `json:"sortable"` Filterable bool `json:"filterable"` Store *auxAttributeStore `json:"store"` Type *auxAttributeType `json:"type"` } // auxAttributeStore is a helper struct used for marshaling/unmarshaling // // This is required since some fields are interfaces auxAttributeStore struct { Type string `json:"type"` Plain *CodecPlain `json:"plain,omitempty"` RecordValueSetJSON *CodecRecordValueSetJSON `json:"recordValueSetJSON,omitempty"` Alias *CodecAlias `json:"alias,omitempty"` } // auxAttributeType is a helper struct used for marshaling/unmarshaling // // This is required since some fields are interfaces auxAttributeType struct { Type string `json:"type"` ID *TypeID `json:"id,omitempty"` Ref *TypeRef `json:"ref,omitempty"` Timestamp *TypeTimestamp `json:"timestamp,omitempty"` Time *TypeTime `json:"time,omitempty"` Date *TypeDate `json:"date,omitempty"` Number *TypeNumber `json:"number,omitempty"` Text *TypeText `json:"text,omitempty"` Boolean *TypeBoolean `json:"boolean,omitempty"` Enum *TypeEnum `json:"enum,omitempty"` Geometry *TypeGeometry `json:"geometry,omitempty"` JSON *TypeJSON `json:"jSON,omitempty"` Blob *TypeBlob `json:"blob,omitempty"` UUID *TypeUUID `json:"uuid,omitempty"` } // auxStoreCodec is a helper struct used for marshaling/unmarshaling // // This is required since some fields are interfaces auxStoreCodec struct { Type string `json:"type"` CodecPlain *CodecPlain `json:"codecPlain"` CodecRecordValueSetJSON *CodecRecordValueSetJSON `json:"codecRecordValueSetJSON"` CodecAlias *CodecAlias `json:"codecAlias"` } AttributeSet []*Attribute Index struct { Ident string Type string Unique bool Fields []*IndexField Predicate string } IndexField struct { AttributeIdent string Modifiers []IndexFieldModifier Sort IndexFieldSort Nulls IndexFieldNulls } IndexSet []*Index IndexFieldModifier string IndexFieldSort int IndexFieldNulls int ) const ( IndexFieldSortDesc IndexFieldSort = -1 IndexFieldSortAsc IndexFieldSort = 1 IndexFieldNullsLast IndexFieldNulls = -1 IndexFieldNullsFirst IndexFieldNulls = 1 IndexFieldModifierLower = "LOWERCASE" ) func PrimaryAttribute(ident string, codec Codec) *Attribute { out := FullAttribute(ident, TypeID{}, codec) out.Type = &TypeID{} out.PrimaryKey = true return out } func FullAttribute(ident string, at Type, codec Codec) *Attribute { return &Attribute{ Ident: ident, Label: ident, Sortable: true, Filterable: true, Store: codec, Type: at, } } func (a *Attribute) WithSoftDelete() *Attribute { a.SoftDeleteFlag = true return a } func (a *Attribute) WithMultiValue() *Attribute { a.MultiValue = true return a } func (a *Attribute) StoreIdent() string { switch s := a.Store.(type) { case *CodecRecordValueSetJSON: return s.Ident case *CodecAlias: return s.Ident default: return a.Ident } } // Compare the two attributes func (a *Attribute) Compare(b *Attribute) bool { if a == nil || b == nil { return a == b } out := true out = out && a.Ident == b.Ident out = out && a.Label == b.Label out = out && a.SensitivityLevelID == b.SensitivityLevelID out = out && a.MultiValue == b.MultiValue out = out && a.PrimaryKey == b.PrimaryKey out = out && a.SoftDeleteFlag == b.SoftDeleteFlag out = out && a.System == b.System out = out && a.Sortable == b.Sortable out = out && a.Filterable == b.Filterable if a.Store == nil || b.Store == nil { out = out && a.Store == b.Store } else { out = out && a.Store.Type() == b.Store.Type() } if a.Type == nil || b.Type == nil { out = out && a.Type == b.Type } else { out = out && a.Type.Type() == b.Type.Type() } return out } func (mm ModelSet) FindByResourceID(resourceID uint64) *Model { for _, m := range mm { if m.ResourceID == resourceID { return m } } return nil } func (mm ModelSet) FindByResourceIdent(resourceType, resourceIdent string) *Model { for _, m := range mm { if m.ResourceType != resourceType { continue } if m.Resource != resourceIdent { continue } return m } return nil } func (mm ModelSet) FindByIdent(ident string) *Model { for _, m := range mm { if m.Ident == ident { return m } } return nil } // FindByRefs returns the first Model that matches the given refs func (mm ModelSet) FindByRefs(refs map[string]any) *Model { for _, model := range mm { for k, v := range refs { ref, ok := model.Refs[k] if !ok { goto skip } if v != ref { goto skip } } return model skip: } 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.Resource == b.Resource { continue } for _, aAttribute := range aModel.Attributes { switch casted := aAttribute.Type.(type) { case *TypeRef: if casted.RefModel.Resource == b.Resource { out = append(out, aModel) } } } } return } func (m Model) ToFilter() ModelRef { return ModelRef{ ConnectionID: m.ConnectionID, ResourceID: m.ResourceID, ResourceType: m.ResourceType, Resource: m.Resource, } } // HasAttribute returns true when the model includes the specified attribute 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 strings.EqualFold(a.Ident, ident) { return a } } return nil } func (aa AttributeSet) FindByStoreIdent(ident string) *Attribute { for _, a := range aa { if strings.EqualFold(a.StoreIdent(), ident) { return a } } return nil } // Validate performs a base model validation before it is passed down func (m Model) Validate() error { if m.Resource == "" { return fmt.Errorf("resource 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 reflect2.IsNil(attr.Type) { return fmt.Errorf("attribute does not define a type: %s", attr.Ident) } } return nil } // Compare the two models // // This only checks model metadata, the attributes are excluded. func (a Model) Compare(b Model) bool { out := true out = out && a.ConnectionID == b.ConnectionID out = out && a.Ident == b.Ident out = out && a.Label == b.Label out = out && a.Resource == b.Resource out = out && a.ResourceID == b.ResourceID out = out && a.ResourceType == b.ResourceType out = out && a.SensitivityLevelID == b.SensitivityLevelID return out } func (a *Attribute) MarshalJSON() ([]byte, error) { aux := &auxAttribute{ Ident: a.Ident, Label: a.Label, SensitivityLevelID: a.SensitivityLevelID, MultiValue: a.MultiValue, PrimaryKey: a.PrimaryKey, SoftDeleteFlag: a.SoftDeleteFlag, System: a.System, Sortable: a.Sortable, Filterable: a.Filterable, Store: &auxAttributeStore{}, Type: &auxAttributeType{}, } switch s := a.Store.(type) { case *CodecPlain: aux.Store.Type = "plain" aux.Store.Plain = s case *CodecRecordValueSetJSON: aux.Store.Type = "recordValueSetJSON" aux.Store.RecordValueSetJSON = s case *CodecAlias: aux.Store.Type = "alias" aux.Store.Alias = s default: return nil, fmt.Errorf("unknown store codec type: %T", s) } switch t := a.Type.(type) { case *TypeID: aux.Type.Type = "ID" aux.Type.ID = t case *TypeRef: aux.Type.Type = "Ref" aux.Type.Ref = t case *TypeTimestamp: aux.Type.Type = "Timestamp" aux.Type.Timestamp = t case *TypeTime: aux.Type.Type = "Time" aux.Type.Time = t case *TypeDate: aux.Type.Type = "Date" aux.Type.Date = t case *TypeNumber: aux.Type.Type = "Number" aux.Type.Number = t case *TypeText: aux.Type.Type = "Text" aux.Type.Text = t case *TypeBoolean: aux.Type.Type = "Boolean" aux.Type.Boolean = t case *TypeEnum: aux.Type.Type = "Enum" aux.Type.Enum = t case *TypeGeometry: aux.Type.Type = "Geometry" aux.Type.Geometry = t case *TypeJSON: aux.Type.Type = "JSON" aux.Type.JSON = t case *TypeBlob: aux.Type.Type = "Blob" aux.Type.Blob = t case *TypeUUID: aux.Type.Type = "UUID" aux.Type.UUID = t default: return nil, fmt.Errorf("unknown attribute type type: %T", t) } return json.Marshal(aux) } func (a *Attribute) UnmarshalJSON(data []byte) (err error) { aux := &auxAttribute{} err = json.Unmarshal(data, &aux) if err != nil { return err } if a == nil { *a = Attribute{} } a.Ident = aux.Ident a.Label = aux.Label a.SensitivityLevelID = aux.SensitivityLevelID a.MultiValue = aux.MultiValue a.PrimaryKey = aux.PrimaryKey a.SoftDeleteFlag = aux.SoftDeleteFlag a.System = aux.System a.Sortable = aux.Sortable a.Filterable = aux.Filterable switch aux.Store.Type { case "plain": a.Store = aux.Store.Plain case "recordValueSetJSON": a.Store = aux.Store.RecordValueSetJSON case "alias": a.Store = aux.Store.Alias } switch aux.Type.Type { case "ID": a.Type = aux.Type.ID case "Ref": a.Type = aux.Type.Ref case "Timestamp": a.Type = aux.Type.Timestamp case "Time": a.Type = aux.Type.Time case "Date": a.Type = aux.Type.Date case "Number": a.Type = aux.Type.Number case "Text": a.Type = aux.Type.Text case "Boolean": a.Type = aux.Type.Boolean case "Enum": a.Type = aux.Type.Enum case "Geometry": a.Type = aux.Type.Geometry case "JSON": a.Type = aux.Type.JSON case "Blob": a.Type = aux.Type.Blob case "UUID": a.Type = aux.Type.UUID default: return fmt.Errorf("unknown attribute type type: %s", aux.Type.Type) } return }