Skip to content

Commit

Permalink
feat: add support modify rest rewrite input value type and resolve en…
Browse files Browse the repository at this point in the history
…um number value on response error.
  • Loading branch information
lovestaryouth committed Feb 17, 2025
1 parent 64e313e commit 1ac6b1b
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 90 deletions.
8 changes: 8 additions & 0 deletions FEATURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,11 @@ query($username: String!) {
## 功能优化:
1. 重写 prisma schema engine 调用逻辑,针对不同方法提供范型约束输入输出

# 版本v2.2.7
## 新增功能:
1. 数据建模页面表列表显示表注释,数据查询、数据编辑/新增显示字段注释
2. 支持x-enum-varnames用以置换枚举变量名,通过enumRealValue标识真实值并在SDK生成真实值

## 问题修复:
1. 数据建模页面在切换到表普通视图,且使用Prisma数据源时,未获取真实数据库类型的问题
2. 修复枚举值为数字时GraphQL解析错误问题
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,6 @@ replace (
github.com/flowchartsman/handlebars/v3 => github.com/fireboomio/handlebars/v3 v3.0.0-20230407011829-1693185f0572
github.com/getkin/kin-openapi => github.com/fireboomio/kin-openapi v0.0.0-20240110095352-e1b4433e41a8
github.com/prisma/prisma-client-go => github.com/fireboomio/prisma-client-go v0.0.0-20240614073744-961bb930abe4
github.com/wundergraph/graphql-go-tools => github.com/fireboomio/graphql-go-tools v0.0.0-20241203014638-126f36a768f3
github.com/wundergraph/graphql-go-tools => github.com/fireboomio/graphql-go-tools v0.0.0-20250214134149-eb9e2b8f1b7d
github.com/wundergraph/wundergraph => ./wundergraphGitSubmodule
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fireboomio/graphql-go-tools v0.0.0-20241203014638-126f36a768f3 h1:+usKCVkPPZAcEU+lSRXw0DyZRRy4X724eNa/6qXWMcM=
github.com/fireboomio/graphql-go-tools v0.0.0-20241203014638-126f36a768f3/go.mod h1:8Lj3WnDT5m+QGamBlqAqwn++OVcL30FXG+o6kf810bc=
github.com/fireboomio/graphql-go-tools v0.0.0-20250214134149-eb9e2b8f1b7d h1:F+S5Jtr29Hty7iVff6wrhETxmbX3rmDOu031nAGcFls=
github.com/fireboomio/graphql-go-tools v0.0.0-20250214134149-eb9e2b8f1b7d/go.mod h1:8Lj3WnDT5m+QGamBlqAqwn++OVcL30FXG+o6kf810bc=
github.com/fireboomio/handlebars/v3 v3.0.0-20230407011829-1693185f0572 h1:utP3HLcMr38qlFFzMwjQeUWM0jxg+MPgktKucOQ2t4I=
github.com/fireboomio/handlebars/v3 v3.0.0-20230407011829-1693185f0572/go.mod h1:zdeQ3Qna7Bd2JQiobPXuZKDOG3j7Byo6uXDaMLXHVAw=
github.com/fireboomio/kin-openapi v0.0.0-20240110095352-e1b4433e41a8 h1:KaancvImcVJEnUVUxDwugkP6F7Yvb2FgasDuVkknZeU=
Expand Down
52 changes: 44 additions & 8 deletions pkg/engine/build/operations_graphql_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"fmt"
"github.com/buger/jsonparser"
"github.com/getkin/kin-openapi/openapi3"
"github.com/spf13/cast"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/formatter"
"github.com/vektah/gqlparser/v2/parser"
Expand Down Expand Up @@ -45,7 +46,6 @@ const (
fieldDefinitionSupplyErrorFormat = "must supply [%s] on path [%s]"
nullableRequiredKey = "Nullable"
selectionRootField = "data"
EnumDescriptionsKey = "X-Enum-Descriptions"
whereUniqueInputSuffix = "WhereUniqueInput"
compoundUniqueInputSuffix = "CompoundUniqueInput"
)
Expand Down Expand Up @@ -749,16 +749,52 @@ func (i *QueryDocumentItem) buildJsonschemaWithDefinition(hasSubFields bool, fie
schemaRef = objectBuild(fieldTypeDefinition)
case ast.Enum:
enumSchema := schemaRef.Value
enumDescriptionMap := make(map[string]string)
enumSchema.Title, enumSchema.Type, enumSchema.Description = fieldTypeName, openapi3.TypeString, fieldTypeDefinition.Description
var (
enumVarnames []interface{}
enumDescriptionMap map[string]string
enumCastMethod func(string) interface{}
)
scalarName, enumDesc := datasource.MatchEnumScalarName(fieldTypeDefinition.Description)
switch scalarName {
case consts.ScalarInt:
enumSchema.Type = openapi3.TypeInteger
enumCastMethod = func(e string) interface{} { return cast.ToInt(e) }
case consts.ScalarFloat:
enumSchema.Type = openapi3.TypeNumber
enumCastMethod = func(e string) interface{} { return cast.ToFloat64(e) }
default:
enumSchema.Type = openapi3.TypeString
}
enumSchema.Title, enumSchema.Description = fieldTypeName, enumDesc
for _, value := range fieldTypeDefinition.EnumValues {
enumSchema.Enum = append(enumSchema.Enum, value.Name)
if len(value.Description) > 0 {
enumDescriptionMap[value.Name] = value.Description
var enumValue interface{}
realValue, description := datasource.MatchEnumRealValue(value.Description)
if len(realValue) > 0 {
enumValue = realValue
enumVarnames = append(enumVarnames, value.Name)
} else {
enumValue = value.Name
}
if enumCastMethod != nil {
enumValue = enumCastMethod(strings.TrimPrefix(value.Name, "_"))
}
enumSchema.Enum = append(enumSchema.Enum, enumValue)

if len(description) > 0 {
if enumDescriptionMap == nil {
enumDescriptionMap = map[string]string{}
}
enumDescriptionMap[value.Name] = description
}
}
if len(enumDescriptionMap) > 0 {
enumSchema.Extensions = map[string]interface{}{EnumDescriptionsKey: enumDescriptionMap}
if len(enumVarnames) > 0 || len(enumDescriptionMap) > 0 {
enumSchema.Extensions = map[string]interface{}{}
if len(enumVarnames) > 0 {
enumSchema.Extensions[datasource.EnumVarnamesKey] = enumVarnames
}
if len(enumDescriptionMap) > 0 {
enumSchema.Extensions[datasource.EnumDescriptionsKey] = enumDescriptionMap
}
}
}
return
Expand Down
67 changes: 34 additions & 33 deletions pkg/engine/datasource/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/labstack/echo/v4"
"github.com/tidwall/gjson"
"github.com/vektah/gqlparser/v2/ast"
"github.com/wundergraph/wundergraph/pkg/customhttpclient"
"github.com/wundergraph/wundergraph/pkg/interpolate"
"github.com/wundergraph/wundergraph/pkg/wgpb"
"golang.org/x/exp/maps"
Expand All @@ -35,13 +36,10 @@ func init() {
customRestExistedResponseRewriters: make(map[string]bool),
customRestRequestRewriterMap: make(map[string]*wgpb.DataSourceCustom_REST_Rewriter),
customRestResponseRewriterMap: make(map[string]*wgpb.DataSourceCustom_REST_Rewriter),
subscribedResolves: make(map[string]resolveOpenapi),
}
}
}

const extensionXSubscribed = "x-subscribed"

var (
versionKeys = []string{"swagger", "openapi"}
yamlExtensions = []fileloader.Extension{fileloader.ExtYaml, fileloader.ExtYml}
Expand All @@ -62,6 +60,7 @@ var (
echo.MIMETextPlainCharsetUTF8,
echo.MIMEMultipartForm,
echo.MIMEOctetStream,
customhttpclient.TextEventStreamMine,
"*/*",
}
)
Expand All @@ -75,7 +74,6 @@ type (
customRestExistedResponseRewriters map[string]bool
customRestRequestRewriterMap map[string]*wgpb.DataSourceCustom_REST_Rewriter
customRestResponseRewriterMap map[string]*wgpb.DataSourceCustom_REST_Rewriter
subscribedResolves map[string]resolveOpenapi
}
resolveOpenapi interface {
resolve(*resolveItem)
Expand Down Expand Up @@ -296,9 +294,23 @@ func (a *actionOpenapi) iteratorPathItem(path string, operation *openapi3.Operat
method: method,
}
resolveInfo.responses = make(map[string]contentSchema)
var (
operationMatchMineTypes []string
operationOnlyEventStream bool
operationContainEventStream bool
)
operationIdSuffix := method.String()
if typeName == consts.TypeSubscription {
operationIdSuffix += typeName
operationMatchMineTypes = append(operationMatchMineTypes, customhttpclient.TextEventStreamMine)
}
for code, resp := range operation.Responses {
respSchema := a.fetchResponseResolveSchema(resp.Value)
if strings.HasPrefix(code, "2") {
respSchema, contentSize := a.fetchResponseResolveSchema(resp.Value, operationMatchMineTypes...)
if code == "200" {
resolveInfo.succeedResponse = respSchema
operationContainEventStream = contentSize > 0 && resp.Value.Content.Get(customhttpclient.TextEventStreamMine) != nil
operationOnlyEventStream = operationContainEventStream && contentSize == 1
} else if strings.HasPrefix(code, "2") && resolveInfo.succeedResponse.schema == nil {
resolveInfo.succeedResponse = respSchema
} else {
resolveInfo.responses[code] = respSchema
Expand All @@ -315,17 +327,6 @@ func (a *actionOpenapi) iteratorPathItem(path string, operation *openapi3.Operat
} else {
resolveInfo.description = operation.Summary
}
var operationIdSuffix string
if typeName == consts.TypeSubscription {
operationIdSuffix = typeName
subscribedBytes, _ := json.Marshal(operation.Extensions[extensionXSubscribed])
_ = json.Unmarshal(subscribedBytes, &resolveInfo.subscribed)
if resolveInfo.subscribed.Schema != nil {
resolveInfo.succeedResponse.schema = a.fetchSchemaRef(resolveInfo.subscribed.Schema)
}
} else {
operationIdSuffix = method.String()
}
resolveInfo.operationId = utils.NormalizeName(utils.JoinString("_", strings.TrimPrefix(operationId, "/"), strings.ToLower(operationIdSuffix)))
resolveInfo.parameters = append(operation.Parameters, parameters...)

Expand All @@ -335,22 +336,21 @@ func (a *actionOpenapi) iteratorPathItem(path string, operation *openapi3.Operat
if resolveInfo.succeedResponse.schema == nil {
resolveInfo.succeedResponse.schema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: openapi3.TypeBoolean, Default: true}}
}
resolve.resolve(resolveInfo)
if typeName == consts.TypeSubscription || !operationOnlyEventStream {
resolve.resolve(resolveInfo)
}

if _, ok := operation.Extensions[extensionXSubscribed]; ok {
if resolvedValue, resolved := a.subscribedResolves[path]; !resolved || resolvedValue != resolve {
a.subscribedResolves[path] = resolve
subscribedInfo := a.iteratorPathItem(path, operation, parameters, method, consts.TypeSubscription, resolve)
if _, ok = a.customRestRequestRewriterMap[subscribedInfo.operationId]; !ok {
if resolvedRewriter, existed := a.customRestRequestRewriterMap[resolveInfo.operationId]; existed {
a.customRestRequestRewriterMap[subscribedInfo.operationId] = resolvedRewriter
}
if typeName != consts.TypeSubscription && operationContainEventStream {
subscribedInfo := a.iteratorPathItem(path, operation, parameters, method, consts.TypeSubscription, resolve)
if _, ok := a.customRestRequestRewriterMap[subscribedInfo.operationId]; !ok {
if resolvedRewriter, existed := a.customRestRequestRewriterMap[resolveInfo.operationId]; existed {
a.customRestRequestRewriterMap[subscribedInfo.operationId] = resolvedRewriter
}
if _, ok = a.customRestResponseRewriterMap[subscribedInfo.operationId]; !ok &&
subscribedInfo.succeedResponse == resolveInfo.succeedResponse {
if resolvedRewriter, existed := a.customRestResponseRewriterMap[resolveInfo.operationId]; existed {
a.customRestResponseRewriterMap[subscribedInfo.operationId] = resolvedRewriter
}
}
if _, ok := a.customRestResponseRewriterMap[subscribedInfo.operationId]; !ok &&
subscribedInfo.succeedResponse == resolveInfo.succeedResponse {
if resolvedRewriter, existed := a.customRestResponseRewriterMap[resolveInfo.operationId]; existed {
a.customRestResponseRewriterMap[subscribedInfo.operationId] = resolvedRewriter
}
}
}
Expand All @@ -359,10 +359,11 @@ func (a *actionOpenapi) iteratorPathItem(path string, operation *openapi3.Operat

// 获取response定义中的schemaRef
// 添加对'*/*'类型的支持
func (a *actionOpenapi) fetchResponseResolveSchema(response *openapi3.Response) (schema contentSchema) {
func (a *actionOpenapi) fetchResponseResolveSchema(response *openapi3.Response, matchType ...string) (schema contentSchema, contentSize int) {
if response != nil && response.Content != nil {
contentSize = len(response.Content)
for _, mime := range mimeTypes {
if result := response.Content.Get(mime); result != nil {
if result := response.Content.Get(mime); result != nil && (len(matchType) == 0 || slices.Contains(matchType, mime)) {
schema.schema, schema.contentType = result.Schema, mime
break
}
Expand Down
83 changes: 75 additions & 8 deletions pkg/engine/datasource/openapi_graphqlschema_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@ import (
"fireboom-server/pkg/common/consts"
"fireboom-server/pkg/common/utils"
"fmt"
"github.com/buger/jsonparser"
"github.com/getkin/kin-openapi/openapi3"
"github.com/spf13/cast"
"github.com/vektah/gqlparser/v2/ast"
"github.com/wundergraph/graphql-go-tools/pkg/engine/plan"
"github.com/wundergraph/wundergraph/pkg/wgpb"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"regexp"
"strings"
)

const (
AdditionalSuffix = "Map"
schemaOneOf = "oneOf"
schemaAnyOf = "anyOf"
schemaAllOf = "allOf"
AdditionalSuffix = "Map"
schemaOneOf = "oneOf"
schemaAnyOf = "anyOf"
schemaAllOf = "allOf"
EnumVarnamesKey = "x-enum-varnames"
EnumDescriptionsKey = "x-enum-descriptions"
)

var (
Expand Down Expand Up @@ -65,6 +70,11 @@ func init() {
stringFormatToScalarMap["date"] = consts.ScalarDate
stringFormatToScalarMap["date-time"] = consts.ScalarDateTime
stringFormatToScalarMap["uuid"] = consts.ScalarUUID

plan.EnumScalarNameFetch = func(desc string) string {
scalarName, _ := MatchEnumScalarName(desc)
return scalarName
}
}

func visitSchemaOneOf(r *resolveGraphqlSchema, input *visitSchemaInput, schemaValue *openapi3.Schema, hasSchemaRefStr bool) visitSchemaOutput {
Expand Down Expand Up @@ -185,19 +195,56 @@ func visitSchemaEnum(r *resolveGraphqlSchema, input *visitSchemaInput, schemaVal

valueRewrites := make(map[string]string)
enumDef.Description = normalizeDescription(schemaValue.Description)
for _, item := range schemaValue.Enum {
switch schemaValue.Type {
case openapi3.TypeInteger:
enumDef.Description += fmt.Sprintf(enumScalarNameFormat, consts.ScalarInt)
case openapi3.TypeNumber:
enumDef.Description += fmt.Sprintf(enumScalarNameFormat, consts.ScalarFloat)
}
var (
enumVarnames []string
enumDescriptions map[string]interface{}
)
if schemaValue.Extensions != nil {
if v, ok := schemaValue.Extensions[EnumVarnamesKey].([]interface{}); ok && len(v) == len(schemaValue.Enum) {
enumVarnames = cast.ToStringSlice(v)
}
if v, ok := schemaValue.Extensions[EnumDescriptionsKey].(map[string]interface{}); ok {
enumDescriptions = v
}
}
for i, item := range schemaValue.Enum {
itemValueDef := &enumValueDefinition{}
enumItem := cast.ToString(item)
itemValueDef.baseDefinition = r.normalizeBaseDefinition(enumItem, "", func(normalized string) {
var (
enumItemValue = enumItem
enumItemDesc string
)
if enumVarnames != nil {
enumItemValue = enumVarnames[i]
enumItemDesc += fmt.Sprintf(enumRealValueFormat, enumItem)
}
if enumDescriptions != nil {
enumItemDesc += cast.ToString(enumDescriptions[enumItem])
}
itemValueDef.baseDefinition = r.normalizeBaseDefinition(enumItemValue, enumItemDesc, func(normalized string) {
valueRewrites[normalized] = enumItem
})
if enumVarnames != nil && itemValueDef.baseDefinition.originName == "" {
itemValueDef.baseDefinition.originName = enumItem
valueRewrites[itemValueDef.baseDefinition.Name] = enumItem
}
enumDef.EnumValues = append(enumDef.EnumValues, itemValueDef)
}
if input.rootKind == ast.InputObject && len(valueRewrites) > 0 {
r.storeCustomRestRewriter(input.rootKind, enumDef.Name, &wgpb.DataSourceRESTRewriter{
valueRewrite := &wgpb.DataSourceRESTRewriter{
Type: wgpb.DataSourceRESTRewriterType_valueRewrite,
ValueRewrites: valueRewrites,
})
}
if schemaValue.Type != openapi3.TypeString {
valueRewrite.ValueType = int32(jsonparser.Number)
}
r.storeCustomRestRewriter(input.rootKind, enumDef.Name, valueRewrite)
}

if schemaValue.Default != nil {
Expand Down Expand Up @@ -411,3 +458,23 @@ func choiceAppendDefinitionField(objectDef *definition, itemDef baseDefinition,
func matchSchemaWithType(schemaRef *openapi3.SchemaRef, schemaType string) bool {
return schemaRef.Value != nil && schemaRef.Value.Type == schemaType
}

const (
enumRealValueFormat = `<#enumRealValue#>%s<#enumRealValue#>`
enumScalarNameFormat = `<#enumScalarName#>%s<#enumScalarName#>`
)

var (
enumRealValueRegexp = regexp.MustCompile(`<#enumRealValue#>([^}]+)<#enumRealValue#>`)
enumScalarNameRegexp = regexp.MustCompile(`<#enumScalarName#>([^}]+)<#enumScalarName#>`)
)

// MatchEnumRealValue 通过在description中添加的特殊标识匹配出额外字段名称
func MatchEnumRealValue(description string) (string, string) {
return utils.MatchNameWithRegexp(description, enumRealValueRegexp)
}

// MatchEnumScalarName 通过在description中添加的特殊标识匹配出额外字段名称
func MatchEnumScalarName(description string) (string, string) {
return utils.MatchNameWithRegexp(description, enumScalarNameRegexp)
}
Loading

0 comments on commit 1ac6b1b

Please sign in to comment.