package database import ( "context" "fmt" "regexp" "strings" "modelRT/common" "modelRT/constants" "modelRT/model" "modelRT/orm" modelsql "modelRT/sql" "gorm.io/gorm" ) var parameterTableNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) // ParameterDataObject contains the resolved metadata needed to query a // parameter attribute after its token has passed hierarchy validation. type ParameterDataObject struct { Component orm.Component Project orm.ProjectManager TableName string AttributeGroup string AttributeName string AttributeType string } // QueryParameterByDataObjectToken validates a four-part or seven-part // parameter token. The component group resolves directly to the component // table; other groups resolve through project_manager and a dynamic table. func QueryParameterByDataObjectToken(ctx context.Context, db *gorm.DB, token string) (*ParameterDataObject, error) { componentQuery, componentArgs, parts, err := buildParameterComponentQuery(token) if err != nil { return nil, err } var components []orm.Component if err := db.WithContext(ctx).Raw(componentQuery, componentArgs...).Scan(&components).Error; err != nil { return nil, fmt.Errorf("query component for parameter token %q: %w", token, err) } switch len(components) { case 0: return nil, fmt.Errorf("%w: component hierarchy for %q", common.ErrParameterTokenNotFound, token) case 1: // Continue by resolving the component model and attribute group. default: return nil, fmt.Errorf("%w: component hierarchy for %q matched more than one record", common.ErrAmbiguousParameterToken, token) } attributeGroup := parts[len(parts)-2] attributeName := parts[len(parts)-1] component := components[0] if attributeGroup == "component" { attributeType, err := queryParameterAttributeType(ctx, db, "component", attributeName, token) if err != nil { return nil, err } return &ParameterDataObject{ Component: component, TableName: "component", AttributeGroup: attributeGroup, AttributeName: attributeName, AttributeType: attributeType, }, nil } var projects []orm.ProjectManager if err := db.WithContext(ctx). Where("tag = ? AND group_name = ?", component.ModelName, attributeGroup). Limit(2). Find(&projects).Error; err != nil { return nil, fmt.Errorf("query project mapping for parameter token %q: %w", token, err) } switch len(projects) { case 0: return nil, fmt.Errorf("%w: model %q does not define attribute group %q", common.ErrParameterTokenNotFound, component.ModelName, attributeGroup) case 1: // Continue by validating the dynamic table and attribute. default: return nil, fmt.Errorf("%w: model %q and attribute group %q matched more than one project", common.ErrAmbiguousParameterToken, component.ModelName, attributeGroup) } project := projects[0] if !parameterTableNamePattern.MatchString(project.Name) { return nil, fmt.Errorf("project mapping for parameter token %q contains invalid table name %q", token, project.Name) } attributeType, err := queryParameterAttributeType(ctx, db, project.Name, attributeName, token) if err != nil { return nil, err } var recordCount int64 if err := db.WithContext(ctx). Table(project.Name). Where("global_uuid = ? AND attribute_group = ?", component.GlobalUUID, attributeGroup). Count(&recordCount).Error; err != nil { return nil, fmt.Errorf("query dynamic record for parameter token %q: %w", token, err) } switch { case recordCount == 0: return nil, fmt.Errorf("%w: component %q has no %q parameter record", common.ErrParameterTokenNotFound, component.Tag, attributeGroup) case recordCount > 1: return nil, fmt.Errorf("%w: component %q has %d %q parameter records", common.ErrAmbiguousParameterToken, component.Tag, recordCount, attributeGroup) } return &ParameterDataObject{ Component: component, Project: project, TableName: project.Name, AttributeGroup: attributeGroup, AttributeName: attributeName, AttributeType: attributeType, }, nil } // QueryParameterDataObjectValue returns token7 from the component row or from // a dynamic parameter row identified during token validation. func QueryParameterDataObjectValue(ctx context.Context, db *gorm.DB, parameter *ParameterDataObject) (any, error) { if parameter == nil { return nil, fmt.Errorf("parameter data object is nil") } var record map[string]any query := db.WithContext(ctx).Table(parameter.TableName) if parameter.AttributeGroup == "component" { query = query.Where("tag = ?", parameter.Component.Tag) } else { query = query.Where("global_uuid = ? AND attribute_group = ?", parameter.Component.GlobalUUID, parameter.AttributeGroup) } result := query.Take(&record) if result.Error != nil { return nil, fmt.Errorf("query parameter value from table %q: %w", parameter.TableName, result.Error) } value, ok := record[parameter.AttributeName] if !ok { return nil, fmt.Errorf("parameter column %q is missing from table %q result", parameter.AttributeName, parameter.TableName) } return value, nil } // QueryParameterAttributeDescription returns the display name registered for // token7 in basic.attribute. func QueryParameterAttributeDescription(ctx context.Context, db *gorm.DB, attributeName string) (string, error) { var rows []struct { Description string `gorm:"column:attribute_name"` } if err := db.WithContext(ctx). Raw(modelsql.ParameterAttributeDescription, attributeName). Scan(&rows).Error; err != nil { return "", fmt.Errorf("query parameter description for attribute %q: %w", attributeName, err) } switch len(rows) { case 0: return "", fmt.Errorf("parameter description not found for attribute %q", attributeName) case 1: return rows[0].Description, nil default: return "", fmt.Errorf("ambiguous parameter description for attribute %q", attributeName) } } func queryParameterAttributeType(ctx context.Context, db *gorm.DB, tableName, attributeName, token string) (string, error) { var attributeType string result := db.WithContext(ctx). Raw(modelsql.ParameterAttributeColumnType, tableName, attributeName). Scan(&attributeType) if result.Error != nil { return "", fmt.Errorf("validate attribute column for parameter token %q: %w", token, result.Error) } if result.RowsAffected == 0 || attributeType == "" { return "", fmt.Errorf("%w: column %q does not exist in parameter table %q", common.ErrParameterTokenNotFound, attributeName, tableName) } return strings.ToUpper(attributeType), nil } func buildParameterComponentQuery(token string) (string, []any, []string, error) { dataObjectType, err := model.ClassifyDataObjectToken(token) if err != nil { return "", nil, nil, fmt.Errorf("%w %q: %v", common.ErrInvalidParameterToken, token, err) } if dataObjectType != constants.DataObjectTypeParameter { return "", nil, nil, fmt.Errorf("%w %q: token does not identify a parameter", common.ErrInvalidParameterToken, token) } parts := strings.Split(token, ".") var where string var args []any switch len(parts) { case 7: where = modelsql.ParameterSevenPartTokenWhere args = []any{parts[0], parts[1], parts[2], parts[3], parts[4]} case 4: where = modelsql.ParameterFourPartTokenWhere args = []any{parts[0], parts[1]} default: return "", nil, nil, fmt.Errorf("%w %q: expected 4 or 7 segments", common.ErrInvalidParameterToken, token) } query := compactParameterSQL(strings.Join([]string{ modelsql.ParameterComponentQueryBase, where, modelsql.ParameterLimitTwo, }, "\n")) return query, args, parts, nil } func compactParameterSQL(statement string) string { return strings.Join(strings.Fields(statement), " ") }