Skip to main content
Glama

Storyden

by Southclaws
Mozilla Public License 2.0
227
properties.go9.02 kB
package library import ( "slices" "strings" "github.com/Southclaws/dt" "github.com/Southclaws/fault" "github.com/Southclaws/fault/ftag" "github.com/Southclaws/opt" "github.com/rs/xid" "github.com/samber/lo" "github.com/Southclaws/storyden/internal/ent" ) type PropertySchemaField struct { ID xid.ID Name string Type PropertyType Sort string } type PropertySchemaFields []*PropertySchemaField type Property struct { Field PropertySchemaField Value opt.Optional[string] } type Properties []*Property type PropertyTable struct { Schema PropertySchema Properties Properties } type PropertySchema struct { ID xid.ID Fields PropertySchemaFields } func (p PropertySchema) FieldIDs() []xid.ID { return dt.Map(p.Fields, func(f *PropertySchemaField) xid.ID { return f.ID }) } func (p PropertySchema) GetField(id xid.ID) (*PropertySchemaField, bool) { lookup := lo.KeyBy(p.Fields, func(f *PropertySchemaField) xid.ID { return f.ID }) f, ok := lookup[id] return f, ok } type PropertySchemaMutation struct { NewProps PropertyMutationList ExistingProps ExistingPropertyMutations RemovedProps ExistingPropertyMutations } // Split takes a mutation (a list of properties to update) and splits it into // two lists, one for properties that need to be added to the schema and current // schema fields which can be processed as simple property update operations. // We also need to get the actual field ID for each existing property. func (p PropertySchema) Split(mutation PropertyMutationList) (*PropertySchemaMutation, error) { fids := lo.FilterMap(mutation, func(p *PropertyMutation, _ int) (xid.ID, bool) { return p.ID.Get() }) existingProperties, newProps := lo.FilterReject(mutation, func(m *PropertyMutation, _ int) bool { return m.ID.Ok() }) // split by existence in the schema. this would be simpler with a DiffBy(). removedIDs, _ := lo.Difference(p.FieldIDs(), fids) existingProps, err := dt.MapErr(existingProperties, p.getSchemaMutationFromPropertyMutation) if err != nil { return nil, fault.Wrap(err) } removedProps, err := dt.MapErr(removedIDs, func(fid xid.ID) (*ExistingPropertyMutation, error) { f, ok := p.GetField(fid) if !ok { return nil, fault.Wrap(fault.Newf("field ID '%v' not found in schema", fid), ftag.With(ftag.InvalidArgument)) } return &ExistingPropertyMutation{ PropertySchemaField: *f, }, nil }) if err != nil { return nil, fault.Wrap(err) } // Check for missing IDs, where the request contains a named property that // exists but the ID is not set. The above code will interpret this as the // field being removed and added again. This is not what we want so error. if len(removedProps) > 0 && len(newProps) > 0 { removedNames := dt.Map(removedProps, func(p *ExistingPropertyMutation) string { return p.Name }) newNames := dt.Map(newProps, func(p *PropertyMutation) string { return p.Name }) // If the intersection of the two lists is not empty, then we have a // conflict where a property is being removed and added again in the same // mutation. This is not allowed. intersect := lo.Intersect(removedNames, newNames) if len(intersect) > 0 { return nil, fault.Wrap( fault.Newf("cannot remove and add the same property in the same mutation: %v did you miss a field ID?", intersect), ftag.With(ftag.InvalidArgument), ) } } return &PropertySchemaMutation{ NewProps: newProps, ExistingProps: existingProps, RemovedProps: removedProps, }, nil } func (p PropertySchema) getSchemaMutationFromPropertyMutation(pm *PropertyMutation) (*ExistingPropertyMutation, error) { f, ok := p.GetField(pm.ID.OrZero()) if !ok { return nil, fault.Wrap(fault.Newf("field '%v' not found in schema", pm.ID), ftag.With(ftag.InvalidArgument)) } // During a property mutation, the request may also change the schema by // changing the name, type or sort properties. Mark as changed if so. isChanged := false // NOTE: A small hack here checking if the mutation name is empty. This will // permit property assignments by just the fid but as a result creates some // undefined behaviour when explicitly setting the field name to "". This // whole code path needs a refactor. if f.Name != pm.Name && pm.Name != "" { isChanged = true f.Name = pm.Name } if t, ok := pm.Type.Get(); ok && t != f.Type { isChanged = true f.Type = t } if s, ok := pm.Sort.Get(); ok && s != f.Sort { isChanged = true f.Sort = s } return &ExistingPropertyMutation{ PropertySchemaField: *f, IsSchemaChanged: isChanged, Value: pm.Value, }, nil } // Property mutations are used to update properties on a node. type PropertyMutation struct { // ID is optional, when set the mutation is modifying an existing field and // when not set, the mutation assumes it's a new field. ID opt.Optional[xid.ID] Name string Value string Type opt.Optional[PropertyType] Sort opt.Optional[string] } type PropertyMutationList []*PropertyMutation type ExistingPropertyMutation struct { PropertySchemaField IsSchemaChanged bool Value string } type ExistingPropertyMutations []*ExistingPropertyMutation func MapPropertyFieldSchema(in PropertySchemaQueryRow) PropertySchemaField { return PropertySchemaField{ ID: in.FieldID, Name: in.Name, Type: in.Type, Sort: in.Sort, } } // PropertySchemaQueryRow is a row from the property schema query which pulls // all the property schemas for both sibling and child properties of a node. type PropertySchemaQueryRow struct { SchemaID xid.ID `db:"schema_id"` FieldID xid.ID `db:"field_id"` Name string `db:"name"` Type PropertyType `db:"type"` Sort string `db:"sort"` Source string `db:"source"` } type PropertySchemaQueryRows []PropertySchemaQueryRow type PropertySchemaTable struct { siblingSchemas PropertySchemaQueryRows childSchemas PropertySchemaQueryRows } // Map harmonises the splits the raw rows into sibling and child schemas. func (r PropertySchemaQueryRows) Map() *PropertySchemaTable { siblings, children := lo.FilterReject(r, func(r PropertySchemaQueryRow, _ int) bool { return r.Source == "sibling" }) return &PropertySchemaTable{ siblingSchemas: siblings, childSchemas: children, } } // BuildPropertyTable yields the properties that are set for the node and also // properties that don't have values by merging in the unused property schemas. func (r *PropertySchemaTable) BuildPropertyTable(in []*ent.Property, isRoot bool) *PropertyTable { if r == nil { return nil } // When mapping a node with children, we fetch the entire list of schemas // from the perspective of the root fetched node. So when mapping properties // we need to switch the source of schemas depending on the mapping context. schemas := r.siblingSchemas if !isRoot { schemas = r.childSchemas } if len(schemas) == 0 { return nil } // Properties are name-unique within a schema (name + schema_id as an index) propMap := lo.KeyBy(schemas, func(r PropertySchemaQueryRow) xid.ID { return r.FieldID }) // Assumption: all schemas for all children are identical, select the first // field to retrieve the schema's ID. schemaID := schemas[0].SchemaID fields := []*Property{} schema := PropertySchema{ ID: schemaID, } // Add all the properties that have values. for _, p := range in { if s, ok := propMap[p.FieldID]; ok { delete(propMap, p.FieldID) fieldSchema := MapPropertyFieldSchema(s) fields = append(fields, &Property{ Field: fieldSchema, Value: opt.New(p.Value), }) schema.Fields = append(schema.Fields, &fieldSchema) } // If a property was not in the schema, ignore it. The member might move // a node back to a parent that had a different schema so we retain data } // Add the remaining property schemas that do not have values. for _, p := range propMap { fieldSchema := MapPropertyFieldSchema(p) schema.Fields = append(schema.Fields, &fieldSchema) fields = append(fields, &Property{ Field: fieldSchema, }) } slices.SortFunc(fields, func(i, j *Property) int { return strings.Compare(i.Field.Sort, j.Field.Sort) }) slices.SortFunc(schema.Fields, func(i, j *PropertySchemaField) int { return strings.Compare(i.Sort, j.Sort) }) return &PropertyTable{ Schema: schema, Properties: fields, } } func (r PropertySchemaTable) ChildSchemas() *PropertySchema { if len(r.childSchemas) == 0 { return nil } // Assumption: all schemas for all children are identical, select the first // field to retrieve the schema's ID. schemaID := r.childSchemas[0].SchemaID fields := dt.Map(r.childSchemas, func(s PropertySchemaQueryRow) *PropertySchemaField { return &PropertySchemaField{ ID: s.FieldID, Name: s.Name, Type: s.Type, Sort: s.Sort, } }) slices.SortFunc(fields, func(i, j *PropertySchemaField) int { return strings.Compare(i.Sort, j.Sort) }) return &PropertySchema{ ID: schemaID, Fields: fields, } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server