diff --git a/cmd/cli.go b/cmd/cli.go index 166f1d8f7..c2455c126 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -3,13 +3,13 @@ package cmd import ( "context" "fmt" + "github.com/jessevdk/go-flags" "github.com/viant/datly/cmd/command" soptions "github.com/viant/datly/cmd/options" ) func RunApp(version string, args soptions.Arguments) error { - options, err := buildOptions(args) if err != nil { return err diff --git a/cmd/datly/build.yaml b/cmd/datly/build.yaml index 9b2fefa03..92ae41d35 100644 --- a/cmd/datly/build.yaml +++ b/cmd/datly/build.yaml @@ -9,7 +9,7 @@ pipeline: set_sdk: action: sdk.set target: $target - sdk: go:1.23 + sdk: go:1.25.1 build: action: exec:run target: $target diff --git a/doc/extension/EXAMPLES.md b/doc/extension/EXAMPLES.md index 1e6d6614a..6f476a0a7 100644 --- a/doc/extension/EXAMPLES.md +++ b/doc/extension/EXAMPLES.md @@ -2200,128 +2200,7 @@ go 1.21 require ( github.com/aerospike/aerospike-client-go v4.5.2+incompatible - github.com/aws/aws-lambda-go v1.31.0 - github.com/francoispqt/gojay v1.2.13 - github.com/go-playground/universal-translator v0.18.0 // indirect - github.com/go-playground/validator v9.31.0+incompatible - github.com/go-sql-driver/mysql v1.7.0 - github.com/goccy/go-json v0.9.11 - github.com/golang-jwt/jwt/v4 v4.4.1 - github.com/google/gops v0.3.23 - github.com/google/uuid v1.3.0 - github.com/jessevdk/go-flags v1.5.0 - github.com/leodido/go-urn v1.2.1 // indirect - github.com/lib/pq v1.10.6 - github.com/mattn/go-sqlite3 v1.14.16 - github.com/onsi/gomega v1.20.2 // indirect - github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.8.4 - github.com/viant/afs v1.24.2 - github.com/viant/afsc v1.9.0 - github.com/viant/assertly v0.9.1-0.20220620174148-bab013f93a60 - github.com/viant/bigquery v0.2.1 - github.com/viant/cloudless v1.8.1 - github.com/viant/dsc v0.16.2 // indirect - github.com/viant/dsunit v0.10.8 - github.com/viant/dyndb v0.1.4-0.20221214043424-27654ab6ed9c - github.com/viant/gmetric v0.2.7-0.20220508155136-c2e3c95db446 - github.com/viant/godiff v0.4.1 - github.com/viant/parsly v0.2.0 - github.com/viant/pgo v0.10.3 - github.com/viant/scy v0.6.0 - github.com/viant/sqlx v0.8.0 - github.com/viant/structql v0.2.2 - github.com/viant/toolbox v0.34.6-0.20221112031702-3e7cdde7f888 - github.com/viant/velty v0.2.0 - github.com/viant/xdatly/types/custom v0.0.0-20230309034540-231985618fc7 - github.com/viant/xreflect v0.0.0-20230303201326-f50afb0feb0d - github.com/viant/xunsafe v0.8.4 - golang.org/x/mod v0.9.0 - golang.org/x/oauth2 v0.7.0 - google.golang.org/api v0.114.0 - gopkg.in/go-playground/assert.v1 v1.2.1 // indirect - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - github.com/viant/govalidator v0.2.1 - github.com/viant/sqlparser v0.3.1-0.20230320162628-96274e82953f - golang.org/x/crypto v0.7.0 // indirect -) - -require ( - github.com/aws/aws-sdk-go v1.44.12 - github.com/aws/aws-sdk-go-v2/config v1.18.3 - github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 - github.com/viant/structology v0.2.0 - github.com/viant/xdatly/extension v0.0.0-20230323215422-3e5c3147f0e6 - github.com/viant/xdatly/handler v0.0.0-20230619231115-e622dd6aff79 - github.com/viant/xdatly/types/core v0.0.0-20230615201419-f5e46b6b011f -) - -require ( - cloud.google.com/go v0.110.0 // indirect - cloud.google.com/go/compute v1.19.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v0.13.0 // indirect - cloud.google.com/go/secretmanager v1.10.0 // indirect - cloud.google.com/go/storage v1.29.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.18.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.7 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.8 // indirect - github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.27 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.20 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sns v1.20.11 // indirect - github.com/aws/aws-sdk-go-v2/service/sqs v1.22.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.11.25 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.17.5 // indirect - github.com/aws/smithy-go v1.13.5 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect - github.com/go-errors/errors v1.4.2 // indirect - github.com/go-playground/locales v0.14.0 // indirect - github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.8.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/kr/pretty v0.3.0 // indirect - github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect - github.com/lestrrat-go/blackmagic v1.0.0 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/iter v1.0.1 // indirect - github.com/lestrrat-go/jwx v1.2.25 // indirect - github.com/lestrrat-go/option v1.0.0 // indirect - github.com/michael/mymodule2 v0.0.0-00010101000000-000000000000 // indirect - github.com/nxadm/tail v1.4.8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect - github.com/viant/igo v0.1.0 // indirect - github.com/yuin/gopher-lua v0.0.0-20221210110428-332342483e3f // indirect - go.opencensus.io v0.24.0 // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/term v0.7.0 // indirect - golang.org/x/text v0.9.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.54.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + .... gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/e2e/local/build.yaml b/e2e/local/build.yaml index b9b3f87b3..78fd0be42 100644 --- a/e2e/local/build.yaml +++ b/e2e/local/build.yaml @@ -14,7 +14,7 @@ pipeline: set_sdk: action: sdk.set target: $target - sdk: go:1.23 + sdk: go:1.25.1 buildValidator: action: exec:run diff --git a/e2e/local/regression/regression.yaml b/e2e/local/regression/regression.yaml index f4ce9a18c..10cbbf501 100644 --- a/e2e/local/regression/regression.yaml +++ b/e2e/local/regression/regression.yaml @@ -5,7 +5,7 @@ pipeline: set_sdk: action: sdk.set target: $target - sdk: go:1.23 + sdk: go:1.25.1 database: action: run diff --git a/gateway/mcp.go b/gateway/mcp.go index 45310d541..d8fe71a0c 100644 --- a/gateway/mcp.go +++ b/gateway/mcp.go @@ -4,6 +4,12 @@ import ( "context" "encoding/json" "fmt" + "io" + "net/http" + "net/url" + "reflect" + "strings" + furl "github.com/viant/afs/url" "github.com/viant/datly/gateway/router/proxy" "github.com/viant/datly/repository" @@ -11,14 +17,10 @@ import ( "github.com/viant/datly/view/state" "github.com/viant/jsonrpc" "github.com/viant/mcp-protocol/authorization" + oauthmeta "github.com/viant/mcp-protocol/oauth2/meta" "github.com/viant/mcp-protocol/schema" serverproto "github.com/viant/mcp-protocol/server" "github.com/viant/toolbox" - "io" - "net/http" - "net/url" - "reflect" - "strings" ) func (r *Router) buildToolsIntegration(item *dpath.Item, aPath *dpath.Path, aRoute *Route, provider *repository.Provider) error { @@ -53,109 +55,258 @@ func (r *Router) buildToolsIntegration(item *dpath.Item, aPath *dpath.Path, aRou } func (r *Router) mcpToolCallHandler(component *repository.Component, aRoute *Route) serverproto.ToolHandlerFunc { - handler := func(ctx context.Context, req *schema.CallToolRequest) (*schema.CallToolResult, *jsonrpc.Error) { + return func(ctx context.Context, req *schema.CallToolRequest) (*schema.CallToolResult, *jsonrpc.Error) { params := req.Params - URI := r.matchToolCallComponentURI(aRoute, component, params) - URL := fmt.Sprintf("http://localhost/%v", strings.TrimLeft(URI, "/")) // fallback to a local URL for now, this should be replaced with the actual service URL + uri := r.matchToolCallComponentURI(aRoute, component, params) + baseURL := fmt.Sprintf("http://localhost/%v", strings.TrimLeft(uri, "/")) // replace with actual service URL when available + values := url.Values{} var body io.Reader - var uniquePath = make(map[string]bool) - var uniqueQuery = make(map[string]bool) - for _, parameter := range component.Input.Type.Parameters { - paramName := strings.Title(parameter.Name) - value := params.Arguments[paramName] - paramType := parameter.Schema.Type() - if paramType.Kind() == reflect.Ptr { - paramType = paramType.Elem() + uniquePath := map[string]bool{} + uniqueQuery := map[string]bool{} + + // 1) Collect parameters (component + selector pagination) + allParams := r.collectToolParameters(component) + + // 2) Apply parameters to request URL/query/body + for _, p := range allParams { + name := strings.Title(p.Name) + value := params.Arguments[name] + pType := p.Schema.Type() + if pType.Kind() == reflect.Ptr { + pType = pType.Elem() } + value = r.coerceNumericValue(value, pType) + var rpcErr *jsonrpc.Error + baseURL, body, rpcErr = r.applyParamToRequest(baseURL, values, p, value, uniquePath, uniqueQuery, body) + if rpcErr != nil { + return nil, rpcErr + } + } - switch paramType.Kind() { - case reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64, reflect.Float64: - if value == nil { - continue - } - value = toolbox.AsInt(value) + // 3) Finalize URL with query string + finalURL := baseURL + if enc := values.Encode(); enc != "" { + if strings.Contains(finalURL, "?") { + finalURL += "&" + enc + } else { + finalURL += "?" + enc } + } - switch parameter.In.Kind { - case state.KindPath: - if uniquePath[parameter.In.Name] { - continue - } - uniquePath[parameter.In.Name] = true + // 4) Build HTTP request and route + httpReq, rpcErr := r.newToolHTTPRequest(aRoute.Path.Method, finalURL, body) + if rpcErr != nil { + return nil, rpcErr + } + r.addAuthTokenIfPresent(ctx, httpReq) - if value == nil { - return nil, jsonrpc.NewInvalidRequest("missing path parameter: "+parameter.In.Name, nil) - } + // NEW: map MCP view sync flag argument to Sync-Read header + r.addSyncReadHeaderIfPresent(ctx, component, ¶ms, httpReq) - URL = strings.ReplaceAll(URL, "{"+parameter.In.Name+"}", fmt.Sprintf("%v", value)) - case state.KindQuery, state.KindForm: - if uniqueQuery[parameter.In.Name] { - continue - } - uniqueQuery[parameter.In.Name] = true - if value == nil || value == "" { - continue - } - // Check if value is a slice and create a comma-separated string - if slice, ok := value.([]interface{}); ok { - var items []string - for _, item := range slice { - if f, ok := item.(float64); ok { - items = append(items, fmt.Sprintf("%v", int64(f))) - } else { - items = append(items, fmt.Sprintf("%v", item)) - } - } - values.Add(parameter.In.Name, strings.Join(items, ",")) - } else { - values.Add(parameter.In.Name, fmt.Sprintf("%v", value)) - } - case state.KindRequestBody: - if text, ok := value.(string); ok { - body = strings.NewReader(text) - } else { - data, err := json.Marshal(value) - if err != nil { - return nil, jsonrpc.NewInvalidParamsError("failed to marshal request body: %w", data) - } - body = strings.NewReader(string(data)) - } + httpReq.RequestURI = httpReq.URL.RequestURI() + if uri != aRoute.URI() { + if matched, _ := r.match(component.Method, uri, httpReq); matched != nil { + aRoute = matched } } - responseWriter := proxy.NewWriter() + rw := proxy.NewWriter() + aRoute.Handle(rw, httpReq) - // Add query parameters to URL if any exist - if len(values) > 0 { - if strings.Contains(URL, "?") { - URL += "&" + values.Encode() - } else { - URL += "?" + values.Encode() + if rw.Code == http.StatusUnauthorized { + return nil, r.mcpUnauthorizedError() + } + + // 5) Build tool result (text + structured on error) + return r.buildToolCallResult(rw, finalURL, aRoute.Path.Method), nil + } +} + +func (r *Router) addSyncReadHeaderIfPresent( + ctx context.Context, + component *repository.Component, + params *schema.CallToolRequestParams, + httpRequest *http.Request, +) { + if params == nil || params.Arguments == nil { + return + } + // MCP tool arguments are generated using exported Go field names, so + // the Datly view sync flag (view.SyncFlag == "viewSyncFlag") will appear + // as "viewSyncFlag" in the schema/tool call. + const mcpSyncFlagArg = "viewSyncFlag" + const headerName = "Sync-Read" + + value, ok := params.Arguments[mcpSyncFlagArg] + if !ok { + return + } + + if !isTruthy(value) { + return + } + + // Optionally, ensure that the underlying component actually declares + // a sync flag parameter; if it does not, we simply skip setting the header. + if !hasSyncFlagParameter(component) { + return + } + + httpRequest.Header.Set(headerName, "true") +} + +// hasSyncFlagParameter checks whether the component declares a selector +// sync flag parameter, which should be exposed as view.SyncFlag. +func hasSyncFlagParameter(component *repository.Component) bool { + if component == nil || component.View == nil || component.View.Selector == nil { + return false + } + param := component.View.Selector.GetSyncFlagParameter() + if param == nil { + return false + } + // The selector sync flag parameter is defined in view.Config using + // view.SyncFlag as the state key, but here we simply check that it exists. + return true +} + +// isTruthy interprets common JSON-serialised truthy values. +func isTruthy(v interface{}) bool { + switch value := v.(type) { + case bool: + return value + case string: + s := strings.TrimSpace(strings.ToLower(value)) + return s == "true" || s == "1" || s == "yes" || s == "y" + case float64: + return value != 0 + default: + return false + } +} + +// collectToolParameters aggregates component input parameters with selector pagination (limit/offset) when available. +func (r *Router) collectToolParameters(component *repository.Component) []*state.Parameter { + var all []*state.Parameter + all = append(all, component.Input.Type.Parameters...) + if component.View != nil && component.View.Selector != nil { + if p := component.View.Selector.LimitParameter; p != nil { + all = append(all, p) + } + if p := component.View.Selector.OffsetParameter; p != nil { + all = append(all, p) + } + if p := component.View.Selector.FieldsParameter; p != nil { + all = append(all, p) + } + if p := component.View.Selector.PageParameter; p != nil { + all = append(all, p) + } + } + return all +} + +// coerceNumericValue normalizes numeric values to integers when appropriate. +func (r *Router) coerceNumericValue(value interface{}, paramType reflect.Type) interface{} { + switch paramType.Kind() { + case reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64, reflect.Float64: + if value == nil { + return nil + } + return toolbox.AsInt(value) + } + return value +} + +// applyParamToRequest applies a single parameter into path placeholders, query/form values, or request body. +func (r *Router) applyParamToRequest(baseURL string, values url.Values, p *state.Parameter, value interface{}, uniquePath, uniqueQuery map[string]bool, body io.Reader) (string, io.Reader, *jsonrpc.Error) { + switch p.In.Kind { + case state.KindPath: + if uniquePath[p.In.Name] { + return baseURL, body, nil + } + uniquePath[p.In.Name] = true + if value == nil { + // If parameter has its own URI segment configured, treat as optional and strip the placeholder. + if p.URI != "" { + baseURL = strings.ReplaceAll(baseURL, "/{"+p.In.Name+"}", "") + baseURL = strings.ReplaceAll(baseURL, "{"+p.In.Name+"}", "") + return baseURL, body, nil } + return baseURL, body, jsonrpc.NewInvalidRequest("missing path parameter: "+p.In.Name, nil) + } + baseURL = strings.ReplaceAll(baseURL, "{"+p.In.Name+"}", fmt.Sprintf("%v", value)) + case state.KindQuery, state.KindForm: + if uniqueQuery[p.In.Name] { + return baseURL, body, nil } - httpRequest, err := http.NewRequest(aRoute.Path.Method, URL, body) - if err != nil { - return nil, jsonrpc.NewInvalidRequest(err.Error(), nil) + uniqueQuery[p.In.Name] = true + if value == nil || value == "" { + return baseURL, body, nil } - r.addAuthTokenIfPresent(ctx, httpRequest) - httpRequest.RequestURI = httpRequest.URL.RequestURI() - if URI != aRoute.URI() { - if matchedRoute, _ := r.match(component.Method, URI, httpRequest); matchedRoute != nil { - aRoute = matchedRoute + if slice, ok := value.([]interface{}); ok { + var items []string + for _, item := range slice { + if f, ok := item.(float64); ok { + items = append(items, fmt.Sprintf("%v", int64(f))) + } else { + items = append(items, fmt.Sprintf("%v", item)) + } } + values.Add(p.In.Name, strings.Join(items, ",")) + } else { + values.Add(p.In.Name, fmt.Sprintf("%v", value)) } - aRoute.Handle(responseWriter, httpRequest) // route the request to the actual handler - var result = schema.CallToolResult{} - mimeType := "application/json" - item := schema.CallToolResultContentElem{ - MimeType: mimeType, - Type: "text", // use data for some clients - Text: responseWriter.Body.String(), + case state.KindRequestBody: + if text, ok := value.(string); ok { + body = strings.NewReader(text) + } else { + data, err := json.Marshal(value) + if err != nil { + return baseURL, body, jsonrpc.NewInvalidParamsError("failed to marshal request body", nil) + } + body = strings.NewReader(string(data)) } - result.Content = append(result.Content, item) - return &result, nil } - return handler + return baseURL, body, nil +} + +// newToolHTTPRequest constructs an HTTP request for routed tool invocation. +func (r *Router) newToolHTTPRequest(method, URL string, body io.Reader) (*http.Request, *jsonrpc.Error) { + httpRequest, err := http.NewRequest(method, URL, body) + if err != nil { + return nil, jsonrpc.NewInvalidRequest(err.Error(), nil) + } + return httpRequest, nil +} + +// buildToolCallResult composes a CallToolResult with text content and structured error info if status is not OK. +func (r *Router) buildToolCallResult(responseWriter *proxy.Writer, URL, method string) *schema.CallToolResult { + var result = &schema.CallToolResult{} + mimeType := responseWriter.HeaderMap.Get("Content-Type") + if mimeType == "" { + mimeType = "application/json" + } + data := responseWriter.Body.Bytes() + result.Content = append(result.Content, schema.CallToolResultContentElem{ + MimeType: mimeType, + Type: "text", + Text: string(data), + }) + _ = json.Unmarshal(data, &result.StructuredContent) + if responseWriter.Code >= http.StatusBadRequest { + isErr := true + result.IsError = &isErr + result.StructuredContent = map[string]interface{}{ + "status": responseWriter.Code, + "error": true, + "message": responseWriter.Body.String(), + "headers": responseWriter.HeaderMap, + "uri": URL, + "method": method, + } + } + return result } func (r *Router) matchToolCallComponentURI(aRoute *Route, component *repository.Component, params schema.CallToolRequestParams) string { @@ -186,10 +337,31 @@ func (r *Router) addAuthTokenIfPresent(ctx context.Context, httpRequest *http.Re } } +const defaultMCPProtectedResource = "https://datly.viantinc.com" + +func (r *Router) mcpUnauthorizedError() *jsonrpc.Error { + if r == nil || r.config == nil || r.config.MCP == nil { + return jsonrpc.NewError(schema.Unauthorized, "Unauthorized", nil) + } + issuerURL := strings.TrimSpace(r.config.MCP.IssuerURL) + if issuerURL == "" { + return jsonrpc.NewError(schema.Unauthorized, "Unauthorized", nil) + } + return jsonrpc.NewError(schema.Unauthorized, "Unauthorized", &authorization.Authorization{ + RequiredScopes: []string{}, + UseIdToken: true, + ProtectedResourceMetadata: &oauthmeta.ProtectedResourceMetadata{ + Resource: defaultMCPProtectedResource, + AuthorizationServers: []string{issuerURL}, + }, + }) +} + func (r *Router) buildToolInputType(components *repository.Component) reflect.Type { var inputFields []reflect.StructField var uniqueQuery = make(map[string]bool) var uniquePath = make(map[string]bool) + // Include component input parameters for _, parameter := range components.Input.Type.Parameters { name := strings.Title(parameter.Name) switch parameter.In.Kind { @@ -198,20 +370,63 @@ func (r *Router) buildToolInputType(components *repository.Component) reflect.Ty continue } uniquePath[parameter.In.Name] = true - inputFields = append(inputFields, reflect.StructField{Name: name, Type: parameter.Schema.Type()}) + // If parameter is a slice, make it optional in schema via `omitempty` and optional:"true". + var tag reflect.StructTag + if parameter.Schema != nil && parameter.Schema.Type().Kind() == reflect.Slice { + tag = `json:",omitempty" optional:"true"` + } + inputFields = append(inputFields, reflect.StructField{Name: name, Type: parameter.Schema.Type(), Tag: tag}) case state.KindQuery, state.KindForm: if uniqueQuery[parameter.In.Name] { continue } uniqueQuery[parameter.In.Name] = true + // Repeated (slice) params are optional regardless of "required" tag. + // Otherwise, respect explicit required; default to optional. tag := reflect.StructTag(parameter.Tag) - if !strings.Contains(parameter.Tag, "required") { + if parameter.Schema != nil && parameter.Schema.Type().Kind() == reflect.Slice { + tag = `json:",omitempty" optional:"true"` + } else if !strings.Contains(parameter.Tag, "required") { tag = `json:",omitempty"` } inputFields = append(inputFields, reflect.StructField{Name: name, Type: parameter.Schema.Type(), Tag: tag}) case state.KindRequestBody: - inputFields = append(inputFields, reflect.StructField{Name: name, Type: parameter.Schema.Type()}) + // If body is a slice, mark optional in schema. + var tag reflect.StructTag + if parameter.Schema != nil && parameter.Schema.Type().Kind() == reflect.Slice { + tag = `json:",omitempty" optional:"true"` + } + inputFields = append(inputFields, reflect.StructField{Name: name, Type: parameter.Schema.Type(), Tag: tag}) + } + } + + // Include selector (limit/offset/fields/page) for read components when available + if components.View != nil && components.View.Selector != nil { + if p := components.View.Selector.LimitParameter; p != nil && p.In != nil && p.In.Name != "" { + if !uniqueQuery[p.In.Name] { // avoid duplicates + uniqueQuery[p.In.Name] = true + inputFields = append(inputFields, reflect.StructField{Name: strings.Title(p.Name), Type: p.Schema.Type(), Tag: `json:",omitempty"`}) + } + } + if p := components.View.Selector.OffsetParameter; p != nil && p.In != nil && p.In.Name != "" { + if !uniqueQuery[p.In.Name] { + uniqueQuery[p.In.Name] = true + inputFields = append(inputFields, reflect.StructField{Name: strings.Title(p.Name), Type: p.Schema.Type(), Tag: `json:",omitempty"`}) + } + } + if p := components.View.Selector.FieldsParameter; p != nil && p.In != nil && p.In.Name != "" { + if !uniqueQuery[p.In.Name] { + uniqueQuery[p.In.Name] = true + // Fields is a []string – ensure optional in schema + inputFields = append(inputFields, reflect.StructField{Name: strings.Title(p.Name), Type: p.Schema.Type(), Tag: `json:",omitempty" optional:"true"`}) + } + } + if p := components.View.Selector.PageParameter; p != nil && p.In != nil && p.In.Name != "" { + if !uniqueQuery[p.In.Name] { + uniqueQuery[p.In.Name] = true + inputFields = append(inputFields, reflect.StructField{Name: strings.Title(p.Name), Type: p.Schema.Type(), Tag: `json:",omitempty"`}) + } } } @@ -229,6 +444,23 @@ func (r *Router) buildTemplateResourceIntegration(item *dpath.Item, aPath *dpath parameterNames = append(parameterNames, parameter.In.Name) } } + // Also expose view selector pagination controls in URI template if present + if provider != nil { + if comp, err := provider.Component(context.Background()); err == nil && comp.View != nil && comp.View.Selector != nil { + if p := comp.View.Selector.LimitParameter; p != nil && p.In != nil && p.In.Name != "" { + parameterNames = append(parameterNames, p.In.Name) + } + if p := comp.View.Selector.OffsetParameter; p != nil && p.In != nil && p.In.Name != "" { + parameterNames = append(parameterNames, p.In.Name) + } + if p := comp.View.Selector.FieldsParameter; p != nil && p.In != nil && p.In.Name != "" { + parameterNames = append(parameterNames, p.In.Name) + } + if p := comp.View.Selector.PageParameter; p != nil && p.In != nil && p.In.Name != "" { + parameterNames = append(parameterNames, p.In.Name) + } + } + } canBuildTemplateResource := len(parameterNames) > 0 || strings.Contains(aPath.URI, "{") if !canBuildTemplateResource { return nil @@ -261,9 +493,9 @@ func (r *Router) buildTemplateResourceIntegration(item *dpath.Item, aPath *dpath func (r *Router) reactMcpResourceHandler(mcpResourceTemplate schema.ResourceTemplate, aRoute *Route, provider *repository.Provider) func(ctx context.Context, request *schema.ReadResourceRequest) (*schema.ReadResourceResult, *jsonrpc.Error) { handler := func(ctx context.Context, request *schema.ReadResourceRequest) (*schema.ReadResourceResult, *jsonrpc.Error) { - result, err := r.handleMcpRead(ctx, &request.Params, &mcpResourceTemplate, aRoute, provider) - if err != nil { - return nil, jsonrpc.NewInternalError(err.Error(), nil) + result, rpcErr := r.handleMcpRead(ctx, &request.Params, &mcpResourceTemplate, aRoute, provider) + if rpcErr != nil { + return nil, rpcErr } if len(result) == 0 { return &schema.ReadResourceResult{Contents: []schema.ReadResourceResultContentsElem{}}, nil @@ -325,12 +557,12 @@ func (r *Router) hasMcpResource(URI string) bool { return false } -func (r *Router) handleMcpRead(ctx context.Context, params *schema.ReadResourceRequestParams, template *schema.ResourceTemplate, aRoute *Route, provider *repository.Provider) ([]schema.ReadResourceResultContentsElem, error) { +func (r *Router) handleMcpRead(ctx context.Context, params *schema.ReadResourceRequestParams, template *schema.ResourceTemplate, aRoute *Route, provider *repository.Provider) ([]schema.ReadResourceResultContentsElem, *jsonrpc.Error) { URI := furl.Path(params.Uri) URL := fmt.Sprintf("http://localhost/%v", URI) // fallback to a local URL for now, this should be replaced with the actual service URL component, err := provider.Component(ctx) // ensure the provider is initialized if err != nil { - return nil, fmt.Errorf("failed to get component from provider: %w", err) + return nil, jsonrpc.NewInternalError(fmt.Errorf("failed to get component from provider: %w", err).Error(), nil) } byLoc := make(map[string]*state.Parameter) for _, param := range component.View.GetResource().Parameters { @@ -344,6 +576,9 @@ func (r *Router) handleMcpRead(ctx context.Context, params *schema.ReadResourceR } r.addAuthTokenIfPresent(ctx, httpRequest) aRoute.Handle(responseWriter, httpRequest) // route the request to the actual handler + if responseWriter.Code == http.StatusUnauthorized { + return nil, r.mcpUnauthorizedError() + } var result []schema.ReadResourceResultContentsElem mimeType := "" if template.MimeType != nil { diff --git a/gateway/route.go b/gateway/route.go index 24e682d3d..9f5cf6eed 100644 --- a/gateway/route.go +++ b/gateway/route.go @@ -2,7 +2,11 @@ package gateway import ( "context" - "github.com/goccy/go-json" + "encoding/json" + "net/http" + "strings" + "time" + "github.com/viant/afs/url" "github.com/viant/datly/gateway/router" "github.com/viant/datly/repository" @@ -11,8 +15,8 @@ import ( "github.com/viant/datly/repository/path" vcontext "github.com/viant/datly/view/context" "github.com/viant/xdatly/handler/exec" - "net/http" - "strings" + + dlogger "github.com/viant/datly/logger" ) const ( @@ -33,6 +37,9 @@ type ( Handler func(ctx context.Context, response http.ResponseWriter, req *http.Request) `json:"-"` logging.Config Version string + + // Counter is an optional per-route metrics counter + Counter dlogger.Counter `json:"-"` } ) @@ -43,7 +50,38 @@ func (r *Route) Handle(res http.ResponseWriter, req *http.Request) int { ctx := context.Background() execContext := exec.NewContext(req.Method, req.RequestURI, req.Header, r.Version) ctx = vcontext.WithValue(ctx, exec.ContextKey, execContext) + var onDone func(time.Time, ...interface{}) int64 = nil + var start time.Time + if r.Counter != nil { + start = time.Now() + onDone = r.Counter.Begin(start) + } r.Handler(ctx, res, req) + + // finalize metrics + if onDone != nil { + end := time.Now() + onDone(end) + // Determine final status code + statusCode := execContext.StatusCode + if statusCode == 0 { + statusCode = http.StatusOK + } + // Increment error/success buckets + if statusCode >= 200 && statusCode < 300 { + r.Counter.IncrementValue("Success") + r.Counter.IncrementValue("status:2xx") + } else if statusCode >= 400 && statusCode < 500 { + r.Counter.IncrementValue("Error") + r.Counter.IncrementValue("status:4xx") + } else if statusCode >= 500 { + r.Counter.IncrementValue("Error") + r.Counter.IncrementValue("status:5xx") + } else { + // Treat other codes as success by default + r.Counter.IncrementValue("Success") + } + } if execContext.StatusCode == 0 { execContext.StatusCode = http.StatusOK } @@ -66,7 +104,7 @@ func (r *Router) NewRouteHandler(handler *router.Handler) *Route { if !strings.HasPrefix(URI, "/") { URI = "/" + URI } - return &Route{ + route := &Route{ Path: &handler.Path.Path, MCP: &handler.Path.ModelContextProtocol, Meta: &handler.Path.Meta, @@ -75,6 +113,9 @@ func (r *Router) NewRouteHandler(handler *router.Handler) *Route { Config: r.config.Logging, Version: r.config.Version, } + // Pre-register and attach per-route counter if metrics are enabled + route.Counter = r.ensureRouteCounter(context.Background(), handler.Provider) + return route } func (r *Route) URI() string { diff --git a/gateway/route_metrics.go b/gateway/route_metrics.go new file mode 100644 index 000000000..213f6c4a3 --- /dev/null +++ b/gateway/route_metrics.go @@ -0,0 +1,73 @@ +package gateway + +import ( + "context" + "path" + "strings" + "time" + + dlogger "github.com/viant/datly/logger" + "github.com/viant/datly/repository" + gprovider "github.com/viant/gmetric/provider" +) + +// ensureRouteCounter pre-registers a per-route counter and returns a logger-compatible adapter. +func (r *Router) ensureRouteCounter(ctx context.Context, prov *repository.Provider) dlogger.Counter { + if r.metrics == nil || prov == nil { + return nil + } + component, err := prov.Component(ctx) + if err != nil || component == nil || component.View == nil { + return nil + } + + v := component.View + + // Derive a stable package from resource URL similar to view.discoverPackage + pkg := "datly" + if res := v.GetResource(); res != nil { + src := res.SourceURL + // Extract the dir and find the segment after "/routes/" + parent, _ := path.Split(src) + if idx := strings.Index(parent, "/routes/"); idx != -1 { + pkg = strings.Trim(parent[idx+len("/routes/"):], "/") + } + } + + // Build a metric operation name aligned with view metrics namespace, but scoped to component URI (.request) + method := component.Path.Method + normURI := normalizeURI(component.URI) + name := strings.Trim(normURI, "/") + ".request" + name = strings.ReplaceAll(name, "/", ".") + metricName := pkg + "." + name + if method != "" && !strings.EqualFold(method, "GET") { + metricName = method + ":" + metricName + } + metricName = strings.ReplaceAll(metricName, "/", ".") + + cnt := r.metrics.LookupOperation(metricName) + if cnt == nil { + // Title: human-friendly + title := v.Name + " request" + cnt = r.metrics.MultiOperationCounter(pkg, metricName, title, time.Millisecond, time.Minute, 2, gprovider.NewBasic()) + } + return dlogger.NewCounter(cnt) +} + +// normalizeURI replaces path parameters like {id} with a constant token to limit cardinality. +func normalizeURI(uri string) string { + res := uri + for { + i := strings.Index(res, "{") + if i == -1 { + break + } + j := strings.Index(res[i:], "}") + if j == -1 { + break + } + j = i + j + 1 + res = res[:i] + "T" + res[j:] + } + return res +} diff --git a/gateway/router.go b/gateway/router.go index 5da821bd0..a63bf8a98 100644 --- a/gateway/router.go +++ b/gateway/router.go @@ -18,11 +18,14 @@ import ( "github.com/viant/datly/repository/path" "github.com/viant/datly/service/operator" "github.com/viant/datly/service/session" + "github.com/viant/datly/shared/logging" "github.com/viant/datly/view" vcontext "github.com/viant/datly/view/context" + "github.com/viant/datly/view/state/kind/locator" "github.com/viant/gmetric" serverproto "github.com/viant/mcp-protocol/server" "github.com/viant/xdatly/handler/async" + "github.com/viant/xdatly/handler/logger" hstate "github.com/viant/xdatly/handler/state" "net/http" @@ -38,6 +41,7 @@ type ( repository *repository.Service operator *operator.Service config *Config + logger logger.Logger OpenAPIInfo openapi3.Info metrics *gmetric.Service statusHandler http.Handler @@ -78,6 +82,7 @@ func NewRouter(ctx context.Context, components *repository.Service, config *Conf operator: operator.New(), apiKeyMatcher: newApiKeyMatcher(config.APIKeys), mcpRegistry: mcpRegistry, + logger: logging.New(logging.INFO, nil), } return r, r.init(ctx) } @@ -154,8 +159,10 @@ func (r *Router) HandleJob(ctx context.Context, aJob *async.Job) error { request := &http.Request{Method: aJob.Method, URL: URL, RequestURI: aPath.URI} unmarshal := aComponent.UnmarshalFunc(request) locatorOptions := append(aComponent.LocatorOptions(request, hstate.NewForm(), unmarshal)) + locatorOptions = append(locatorOptions, locator.WithLogger(r.logger)) aSession := session.New(aComponent.View, session.WithAuth(r.repository.Auth()), + session.WithLogger(r.logger), session.WithComponent(aComponent), session.WithLocatorOptions(locatorOptions...), session.WithOperate(r.operator.Operate)) @@ -342,7 +349,7 @@ func (r *Router) newMatcher(ctx context.Context) (*matcher.Matcher, []*contract. } r.EnsureCors(aPath) - aRoute := r.NewRouteHandler(router.New(aPath, provider, r.repository.Registry(), r.repository.Auth(), r.config.Version, r.config.Logging)) + aRoute := r.NewRouteHandler(router.New(aPath, provider, r.repository.Registry(), r.repository.Auth(), r.config.Version, r.config.Logging, r.logger)) routes = append(routes, aRoute) if aPath.Cors != nil { optionsPaths[aPath.URI] = append(optionsPaths[aPath.URI], aPath) diff --git a/gateway/router/handler.go b/gateway/router/handler.go index b45917b65..ef3b129c3 100644 --- a/gateway/router/handler.go +++ b/gateway/router/handler.go @@ -27,7 +27,9 @@ import ( "github.com/viant/datly/view" vcontext "github.com/viant/datly/view/context" "github.com/viant/datly/view/state" + "github.com/viant/datly/view/state/kind/locator" "github.com/viant/xdatly/handler/exec" + "github.com/viant/xdatly/handler/logger" "github.com/viant/xdatly/handler/response" hstate "github.com/viant/xdatly/handler/state" "io" @@ -54,6 +56,7 @@ type ( registry *repository.Registry auth *auth.Service logging logging.Config + logger logger.Logger } ) @@ -87,7 +90,7 @@ func (r *Handler) AuthorizeRequest(request *http.Request, aPath *path.Path) erro return nil } -func New(aPath *path.Path, provider *repository.Provider, registry *repository.Registry, authService *auth.Service, version string, config logging.Config) *Handler { +func New(aPath *path.Path, provider *repository.Provider, registry *repository.Registry, authService *auth.Service, version string, config logging.Config, logger logger.Logger) *Handler { ret := &Handler{ Path: aPath, Provider: provider, @@ -96,6 +99,7 @@ func New(aPath *path.Path, provider *repository.Provider, registry *repository.R auth: authService, Version: version, logging: config, + logger: logger, } return ret } @@ -257,11 +261,8 @@ func (r *Handler) writeErrorResponse(ctx context.Context, w http.ResponseWriter, http.Error(w, err.Error(), http.StatusInternalServerError) return } - if aComponent.Content.Marshaller.JSON.CanMarshal() { - data, err = aComponent.Marshaller.JSON.Codec.Marshal(aResponse.State()) - } else { - data, err = aComponent.Marshaller.JSON.JsonMarshaller.Marshal(aResponse.State()) - } + mf := aComponent.MarshalFunc() + data, err = mf(aResponse.State()) if err != nil { w.Write(data) if execCtx != nil { @@ -390,11 +391,14 @@ func (r *Handler) handleComponent(ctx context.Context, request *http.Request, aC anOperator := operator.New() unmarshal := aComponent.UnmarshalFunc(request) locatorOptions := append(aComponent.LocatorOptions(request, hstate.NewForm(), unmarshal)) + locatorOptions = append(locatorOptions, locator.WithLogger(r.logger)) aSession := session.New(aComponent.View, session.WithAuth(r.auth), + session.WithLogger(r.logger), session.WithComponent(aComponent), session.WithLocatorOptions(locatorOptions...), session.WithRegistry(r.registry), + session.WithOperate(anOperator.Operate)) err := aSession.InitKinds(state.KindComponent, state.KindHeader, state.KindRequestBody, state.KindForm, state.KindQuery) if err != nil { @@ -455,8 +459,10 @@ func (r *Handler) handleComponent(ctx context.Context, request *http.Request, aC options.Append(response.WithHeader("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.xlsx"`, aComponent.Output.GetTitle()))) } } + // Use component-level marshaller with request-scoped options filters := aComponent.Exclusion(aSession.State()) - data, err := aComponent.Content.Marshal(format, aComponent.Output.Field(), output, filters) + mf := aComponent.MarshalFunc(repository.WithRequest(request), repository.WithFormat(format), repository.WithFilters(filters)) + data, err := mf(output) if err != nil { return nil, response.NewError(500, fmt.Sprintf("failed to marshal response: %v", err), response.WithError(err)) } @@ -494,13 +500,9 @@ func (r *Handler) marshalComponentOutput(output interface{}, aComponent *reposit case []byte: return response.NewBuffered(response.WithBytes(actual)), nil default: - var data []byte - var err error - if aComponent.Content.Marshaller.JSON.CanMarshal() { - data, err = aComponent.Content.Marshaller.JSON.Codec.Marshal(output) - } else { - data, err = aComponent.Content.Marshaller.JSON.JsonMarshaller.Marshal(output) - } + // Default to JSON marshalling using component-level marshaller + mf := aComponent.MarshalFunc() + data, err := mf(output) if err != nil { return nil, response.NewError(http.StatusInternalServerError, err.Error(), response.WithError(err)) } diff --git a/gateway/router/marshal/json/cache.go b/gateway/router/marshal/json/cache.go index cd79eba61..15c46e4b9 100644 --- a/gateway/router/marshal/json/cache.go +++ b/gateway/router/marshal/json/cache.go @@ -3,13 +3,14 @@ package json import ( "bytes" "fmt" + "reflect" + "sync" + "github.com/viant/datly/gateway/router/marshal/config" "github.com/viant/tagly/format" "github.com/viant/tagly/format/text" "github.com/viant/xreflect" "github.com/viant/xunsafe" - "reflect" - "sync" ) var buffersPool *buffers @@ -99,30 +100,39 @@ func (m *marshallersCache) loadMarshaller(rType reflect.Type, config *config.IOC return marshaller, nil } -func (c *pathCache) loadOrGetMarshaller(rType reflect.Type, config *config.IOConfig, path string, outputPath string, tag *format.Tag, options ...interface{}) (marshaler, error) { - value, ok := c.cache.Load(rType) +func (c *pathCache) loadOrGetMarshaller(rType reflect.Type, cfg *config.IOConfig, path, outPath string, tag *format.Tag, options ...interface{}) (marshaler, error) { + + placeholder := newDeferred() + value, ok := c.cache.LoadOrStore(rType, placeholder) if ok { return value.(marshaler), nil } - aMarshaler, err := c.getMarshaller(rType, config, path, outputPath, tag, options...) - + aMarshaller, err := c.getMarshaller(rType, cfg, path, outPath, tag, options...) if err != nil { + placeholder.fail(err) // unblock anyone holding the promise + c.cache.CompareAndDelete(rType, placeholder) // allow a clean retry later return nil, err } - c.storeMarshaler(rType, aMarshaler) - return aMarshaler, nil + placeholder.setTarget(aMarshaller) // resolve success + return aMarshaller, nil } func (c *pathCache) getMarshaller(rType reflect.Type, config *config.IOConfig, path string, outputPath string, tag *format.Tag, options ...interface{}) (marshaler, error) { + if rType == nil { + return nil, fmt.Errorf("nil reflect.Type for path %q", path) + } if tag == nil { tag = &format.Tag{} } aConfig := c.parseConfig(options) - if (aConfig == nil || !aConfig.ignoreCustomUnmarshaller) && rType.Implements(unmarshallerIntoType) { - return newCustomUnmarshaller(rType, config, path, outputPath, tag, c.parent) + // Keep UnmarshalerInto precedence for non-structs; structs handled below to honor gojay first. + if rType.Kind() != reflect.Struct { + if (aConfig == nil || !aConfig.IgnoreCustomUnmarshaller) && rType.Implements(unmarshallerIntoType) { + return newCustomUnmarshaller(rType, config, path, outputPath, tag, c.parent) + } } switch rType { @@ -212,12 +222,35 @@ func (c *pathCache) getMarshaller(rType reflect.Type, config *config.IOConfig, p return newTimeMarshaller(tag, config), nil } - marshaller, err := newStructMarshaller(config, rType, path, outputPath, tag, c.parent) + // Decide if type uses gojay; build base without init to handle self-references safely. + hasMarshal := (aConfig == nil || !aConfig.IgnoreCustomMarshaller) && (rType.Implements(marshalerJSONObjectType) || reflect.PtrTo(rType).Implements(marshalerJSONObjectType)) + hasUnmarshal := (aConfig == nil || !aConfig.IgnoreCustomMarshaller) && (rType.Implements(unmarshalerJSONObjectType) || reflect.PtrTo(rType).Implements(unmarshalerJSONObjectType)) + + base, err := newStructMarshaller(config, rType, path, outputPath, tag, c.parent) if err != nil { return nil, err } - return marshaller, nil + if hasMarshal || hasUnmarshal { + // Wrap base with gojay; placeholder at loadOrGet level already breaks cycles. + wrapper := newGojayObjectMarshaller(getXType(rType), getXType(reflect.PtrTo(rType)), base, hasMarshal, hasUnmarshal) + if err := base.init(); err != nil { + return nil, err + } + return wrapper, nil + } + + // No gojay: just init base and return (placeholder already in place). + if err := base.init(); err != nil { + return nil, err + } + + // Allow custom unmarshaller on structs if defined and not ignored (only if no gojay used). + if (aConfig == nil || !aConfig.IgnoreCustomUnmarshaller) && rType.Implements(unmarshallerIntoType) { + return newCustomUnmarshaller(rType, config, path, outputPath, tag, c.parent) + } + + return base, nil case reflect.Interface: marshaller, err := newInterfaceMarshaller(rType, config, path, outputPath, tag, c.parent) diff --git a/gateway/router/marshal/json/init.go b/gateway/router/marshal/json/init.go index cacb47602..31eebb40e 100644 --- a/gateway/router/marshal/json/init.go +++ b/gateway/router/marshal/json/init.go @@ -13,6 +13,8 @@ import ( var rawMessageType = reflect.TypeOf(json.RawMessage{}) var unmarshallerIntoType = reflect.TypeOf((*UnmarshalerInto)(nil)).Elem() +var marshalerJSONObjectType = reflect.TypeOf((*gojay.MarshalerJSONObject)(nil)).Elem() +var unmarshalerJSONObjectType = reflect.TypeOf((*gojay.UnmarshalerJSONObject)(nil)).Elem() var mapStringIfaceType = reflect.TypeOf(map[string]interface{}{}) var decData *xunsafe.Field var decCur *xunsafe.Field diff --git a/gateway/router/marshal/json/marshaller_bool_ptr.go b/gateway/router/marshal/json/marshaller_bool_ptr.go index 86a562a9d..9a54f47f6 100644 --- a/gateway/router/marshal/json/marshaller_bool_ptr.go +++ b/gateway/router/marshal/json/marshaller_bool_ptr.go @@ -35,5 +35,5 @@ func (i *boolPtrMarshaller) MarshallObject(ptr unsafe.Pointer, sb *MarshallSessi } func (i *boolPtrMarshaller) UnmarshallObject(pointer unsafe.Pointer, decoder *gojay.Decoder, auxiliaryDecoder *gojay.Decoder, session *UnmarshalSession) error { - return decoder.AddBool(xunsafe.AsBoolPtr(pointer)) + return decoder.AddBoolNull(xunsafe.AsBoolAddrPtr(pointer)) } diff --git a/gateway/router/marshal/json/marshaller_custom.go b/gateway/router/marshal/json/marshaller_custom.go index eb73890c3..81ca8fbd8 100644 --- a/gateway/router/marshal/json/marshaller_custom.go +++ b/gateway/router/marshal/json/marshaller_custom.go @@ -21,7 +21,7 @@ type customMarshaller struct { } func newCustomUnmarshaller(rType reflect.Type, config *config.IOConfig, path string, outputPath string, tag *format.Tag, cache *marshallersCache) (marshaler, error) { - marshaller, err := cache.loadMarshaller(rType, config, path, outputPath, tag, &cacheConfig{ignoreCustomUnmarshaller: true}) + marshaller, err := cache.loadMarshaller(rType, config, path, outputPath, tag, &cacheConfig{IgnoreCustomUnmarshaller: true}) if err != nil { return nil, err } diff --git a/gateway/router/marshal/json/marshaller_deferred.go b/gateway/router/marshal/json/marshaller_deferred.go new file mode 100644 index 000000000..48955de80 --- /dev/null +++ b/gateway/router/marshal/json/marshaller_deferred.go @@ -0,0 +1,57 @@ +package json + +import ( + "fmt" + "unsafe" + + "github.com/francoispqt/gojay" +) + +// deferredMarshaller is a placeholder used to break recursive type graphs during construction. +// It forwards calls to the actual target once it is set. +type deferredMarshaller struct { + target marshaler + ready chan struct{} + err error +} + +func newDeferred() *deferredMarshaller { + return &deferredMarshaller{ready: make(chan struct{})} +} + +func (d *deferredMarshaller) setTarget(m marshaler) { + d.target = m + close(d.ready) +} + +func (d *deferredMarshaller) fail(e error) { + d.err = e + close(d.ready) // writes to err happen-before any receive on ready +} + +func (d *deferredMarshaller) resolved() (marshaler, error) { + <-d.ready // wait for resolve/fail + if d.err != nil { + return nil, d.err + } + if d.target == nil { + return nil, fmt.Errorf("marshaller not initialized") + } + return d.target, nil +} + +func (d *deferredMarshaller) MarshallObject(ptr unsafe.Pointer, s *MarshallSession) error { + m, err := d.resolved() + if err != nil { + return err + } + return m.MarshallObject(ptr, s) +} + +func (d *deferredMarshaller) UnmarshallObject(p unsafe.Pointer, dec, aux *gojay.Decoder, s *UnmarshalSession) error { + m, err := d.resolved() + if err != nil { + return err + } + return m.UnmarshallObject(p, dec, aux, s) +} diff --git a/gateway/router/marshal/json/marshaller_gojay_object.go b/gateway/router/marshal/json/marshaller_gojay_object.go new file mode 100644 index 000000000..af3cbbec3 --- /dev/null +++ b/gateway/router/marshal/json/marshaller_gojay_object.go @@ -0,0 +1,68 @@ +package json + +import ( + "github.com/francoispqt/gojay" + "github.com/viant/xunsafe" + "unsafe" +) + +// gojayObjectMarshaller delegates to gojay's Marshaler/UnmarshalerJSONObject when available, +// and falls back to the generic struct marshaller for the other direction. +type gojayObjectMarshaller struct { + valueType *xunsafe.Type + addrType *xunsafe.Type + fallback marshaler + useMarshal bool + useUnmarshal bool +} + +func newGojayObjectMarshaller(valueType *xunsafe.Type, addrType *xunsafe.Type, fallback marshaler, useMarshal, useUnmarshal bool) *gojayObjectMarshaller { + return &gojayObjectMarshaller{ + valueType: valueType, + addrType: addrType, + fallback: fallback, + useMarshal: useMarshal, + useUnmarshal: useUnmarshal, + } +} + +func (g *gojayObjectMarshaller) MarshallObject(ptr unsafe.Pointer, session *MarshallSession) error { + if ptr == nil { + session.Write(nullBytes) + return nil + } + + if g.useMarshal { + // Prefer pointer receiver if (*T) implements MarshalerJSONObject + if m, ok := g.addrType.Value(ptr).(gojay.MarshalerJSONObject); ok { + enc := gojay.NewEncoder(session.Buffer) + return enc.EncodeObject(m) + } + // Fallback to value receiver if (T) implements MarshalerJSONObject + if m, ok := g.valueType.Interface(ptr).(gojay.MarshalerJSONObject); ok { + enc := gojay.NewEncoder(session.Buffer) + return enc.EncodeObject(m) + } + // If neither matched at runtime, fallback to generic marshaller + } + return g.fallback.MarshallObject(ptr, session) +} + +func (g *gojayObjectMarshaller) UnmarshallObject(pointer unsafe.Pointer, decoder *gojay.Decoder, auxiliaryDecoder *gojay.Decoder, session *UnmarshalSession) error { + if !g.useUnmarshal { + return g.fallback.UnmarshallObject(pointer, decoder, auxiliaryDecoder, session) + } + + d := decoder + if auxiliaryDecoder != nil { + d = auxiliaryDecoder + } + + // Prefer pointer receiver only; value receiver cannot mutate destination reliably. + if u, ok := g.addrType.Value(pointer).(gojay.UnmarshalerJSONObject); ok { + return d.Object(u) + } + + // If neither matched at runtime, fallback to generic unmarshaller + return g.fallback.UnmarshallObject(pointer, decoder, auxiliaryDecoder, session) +} diff --git a/gateway/router/marshal/json/marshaller_interface.go b/gateway/router/marshal/json/marshaller_interface.go index c7da33fe9..4256327c2 100644 --- a/gateway/router/marshal/json/marshaller_interface.go +++ b/gateway/router/marshal/json/marshaller_interface.go @@ -47,7 +47,15 @@ func asInterface(xType *xunsafe.Type, pointer unsafe.Pointer) interface{} { func (i *interfaceMarshaller) MarshallObject(ptr unsafe.Pointer, sb *MarshallSession) error { value := i.AsInterface(ptr) + if value == nil { + sb.Write(nullBytes) + return nil + } rType := reflect.TypeOf(value) + if rType == nil { + sb.Write(nullBytes) + return nil + } marshaller, err := i.cache.loadMarshaller(rType, i.config, i.path, i.outputPath, i.tag) if err != nil { diff --git a/gateway/router/marshal/json/marshaller_map.go b/gateway/router/marshal/json/marshaller_map.go index 320001bbe..427868bf1 100644 --- a/gateway/router/marshal/json/marshaller_map.go +++ b/gateway/router/marshal/json/marshaller_map.go @@ -203,6 +203,9 @@ func (m *mapMarshaller) mapStringIfaceMarshaller() func(pointer unsafe.Pointer, return nil } + // Ensure JSON special characters in keys are escaped + replacer := getReplacer() + if !m.isEmbedded { sb.WriteString("{") } @@ -214,9 +217,9 @@ func (m *mapMarshaller) mapStringIfaceMarshaller() func(pointer unsafe.Pointer, sb.WriteString(",") } counter++ - sb.WriteString(`"`) - sb.WriteString(namesIndex.formatTo(aKey, m.config.CaseFormat)) - sb.WriteString(`":`) + // Write escaped key + marshallString(namesIndex.formatTo(aKey, m.config.CaseFormat), sb, replacer) + sb.WriteString(`:`) if err := m.valueMarshaller.MarshallObject(AsPtr(aValue, m.valueType), sb); err != nil { return err diff --git a/gateway/router/marshal/json/marshaller_raw_message.go b/gateway/router/marshal/json/marshaller_raw_message.go index 5ed57e11f..75686d0c2 100644 --- a/gateway/router/marshal/json/marshaller_raw_message.go +++ b/gateway/router/marshal/json/marshaller_raw_message.go @@ -1,6 +1,7 @@ package json import ( + stdjson "encoding/json" "github.com/francoispqt/gojay" "github.com/viant/xunsafe" "unsafe" @@ -14,12 +15,16 @@ func newRawMessageMarshaller() *rawMessageMarshaller { func (r *rawMessageMarshaller) UnmarshallObject(pointer unsafe.Pointer, decoder *gojay.Decoder, auxiliaryDecoder *gojay.Decoder, session *UnmarshalSession) error { bytesPtr := xunsafe.AsBytesPtr(pointer) - dst := "" - if err := decoder.DecodeString(&dst); err != nil { + // Decode arbitrary JSON value into interface{}, then re-marshal to raw bytes. + var val interface{} + if err := decoder.AddInterface(&val); err != nil { return err } - - *bytesPtr = []byte(dst) + data, err := stdjson.Marshal(val) + if err != nil { + return err + } + *bytesPtr = data return nil } diff --git a/gateway/router/marshal/json/marshaller_slice.go b/gateway/router/marshal/json/marshaller_slice.go index 1ad341cef..51d90d3f4 100644 --- a/gateway/router/marshal/json/marshaller_slice.go +++ b/gateway/router/marshal/json/marshaller_slice.go @@ -151,7 +151,15 @@ func (s *sliceInterfaceMarshaller) MarshallObject(ptr unsafe.Pointer, sb *Marsha sb.WriteByte(',') } + if iface == nil { + sb.Write(nullBytes) + continue + } ifaceType := reflect.TypeOf(iface) + if ifaceType == nil { + sb.Write(nullBytes) + continue + } marshaller, err := s.cache.loadMarshaller(ifaceType, s.config, s.path, s.outputPath, s.tag) if err != nil { diff --git a/gateway/router/marshal/json/marshaller_strings.go b/gateway/router/marshal/json/marshaller_strings.go index c044fc5b6..b0ba6fcea 100644 --- a/gateway/router/marshal/json/marshaller_strings.go +++ b/gateway/router/marshal/json/marshaller_strings.go @@ -1,11 +1,12 @@ package json import ( + "strings" + "unsafe" + "github.com/francoispqt/gojay" "github.com/viant/tagly/format" "github.com/viant/xunsafe" - "strings" - "unsafe" ) type stringMarshaller struct { @@ -49,16 +50,60 @@ func (i *stringMarshaller) ensureReplacer() { } } -func marshallString(asString string, sb *MarshallSession, replacer *strings.Replacer) { +func marshallString(asString string, sb *MarshallSession, _ *strings.Replacer) { + // Fully JSON-escape the string, including control chars and JS line/paragraph separators. + const hexDigits = "0123456789abcdef" sb.WriteByte('"') - sb.WriteString(replacer.Replace(asString)) + for i := 0; i < len(asString); i++ { + c := asString[i] + switch c { + case '\\', '"': + sb.WriteByte('\\') + sb.WriteByte(c) + case '/': + sb.WriteByte('\\') + sb.WriteByte('/') + case '\b': + sb.WriteString(`\\b`) + case '\f': + sb.WriteString(`\\f`) + case '\n': + sb.WriteString(`\n`) + case '\r': + sb.WriteString(`\\r`) + case '\t': + sb.WriteString(`\t`) + default: + // Escape other control characters < 0x20 as \u00XX + if c < 0x20 { + sb.WriteString(`\\u00`) + sb.WriteByte(hexDigits[c>>4]) + sb.WriteByte(hexDigits[c&0x0F]) + continue + } + // Escape U+2028 and U+2029 to be safe for JS embed contexts + if c == 0xE2 && i+2 < len(asString) { + c1 := asString[i+1] + c2 := asString[i+2] + if c1 == 0x80 && (c2 == 0xA8 || c2 == 0xA9) { + if c2 == 0xA8 { + sb.WriteString(`\\u2028`) + } else { + sb.WriteString(`\\u2029`) + } + i += 2 + continue + } + } + sb.WriteByte(c) + } + } sb.WriteByte('"') } func getReplacer() *strings.Replacer { return strings.NewReplacer(`\`, `\\`, `"`, `\"`, - `/`, `\/`, "\b", `\b`, "\f", `\f`, "\n", `\n`, diff --git a/gateway/router/marshal/json/marshaller_struct.go b/gateway/router/marshal/json/marshaller_struct.go index a0d2d1070..e3d962191 100644 --- a/gateway/router/marshal/json/marshaller_struct.go +++ b/gateway/router/marshal/json/marshaller_struct.go @@ -1,16 +1,18 @@ package json import ( + "reflect" + "strings" + "unicode" + "unsafe" + "github.com/francoispqt/gojay" "github.com/viant/datly/gateway/router/marshal/config" + "github.com/viant/datly/view/tags" structology "github.com/viant/structology" "github.com/viant/tagly/format" "github.com/viant/tagly/format/text" xunsafe "github.com/viant/xunsafe" - "reflect" - "strings" - "unicode" - "unsafe" ) type ( @@ -68,7 +70,9 @@ func newStructMarshaller(config *config.IOConfig, rType reflect.Type, path strin marshallersIndex: map[string]int{}, } - return result, result.init() + // Initialization is invoked by cache after it stores the marshaller (or wrapper) + // to break cycles for self-referential types. + return result, nil } func (s *structMarshaller) UnmarshallObject(pointer unsafe.Pointer, decoder *gojay.Decoder, auxiliaryDecoder *gojay.Decoder, session *UnmarshalSession) error { @@ -252,10 +256,19 @@ func (s *structMarshaller) createStructMarshallers(fields *groupedFields, path s if err != nil { return nil, err } + if dTag.Name == "" { //fallback to parameter + if parameterTag := field.Tag.Get("parameter"); parameterTag != "" { + if aTag, _ := tags.Parse(field.Tag, nil, tags.ParameterTag); aTag != nil && aTag.Parameter != nil { + if aTag.Parameter.Kind == "body" { + dTag.Name = aTag.Parameter.In + } + } + } + } elemType := field.Type - switch elemType.Kind() { - case reflect.Ptr, reflect.Slice: + // Unwrap nested pointers/slices to detect self-references like []*T or [][]*T + for elemType.Kind() == reflect.Ptr || elemType.Kind() == reflect.Slice { elemType = elemType.Elem() } if elemType == fields.owner { @@ -313,6 +326,7 @@ func (s *structMarshaller) newFieldMarshaller(marshallers *[]*marshallerWithFiel } else if s.config.CaseFormat != "" { jsonName = formatName(jsonName, s.config.CaseFormat) } + path, outputPath = addToPath(path, field.Name), addToPath(outputPath, jsonName) xField := xunsafe.NewField(field) diff --git a/gateway/router/marshal/json/marshaller_struct_test.go b/gateway/router/marshal/json/marshaller_struct_test.go new file mode 100644 index 000000000..845b1e433 --- /dev/null +++ b/gateway/router/marshal/json/marshaller_struct_test.go @@ -0,0 +1,169 @@ +package json + +import ( + stdjson "encoding/json" + "reflect" + "testing" + "time" + + "github.com/viant/datly/gateway/router/marshal/config" + "github.com/viant/tagly/format/text" +) + +// Session represents a user session document. +type Session struct { + // UserID is the PK of the session set. + UserID int `aerospike:"user_id,pk"` + // LastSeen is the last activity timestamp. Stored as unix seconds. + LastSeen *time.Time `aerospike:"last_seen,unixsec"` + // Disabled marks the session as inactive. + Disabled *bool `aerospike:"disabled"` + // Attribute holds session attributes entries. + Attribute []Attribute +} + +// Attribute represents a single attribute entry stored within the session's attributes map bin. +// The PK is still `user_id`, and attribute entries are keyed by `name`. +type Attribute struct { + // UserID is the session owner and record key. + UserID int `aerospike:"user_id,pk"` + // Name is the attribute key (map key). + Name *string `aerospike:"name,mapKey"` + // Value is the attribute payload; supports native Aerospike types. + Value stdjson.RawMessage `aerospike:"value"` +} + +func newMarshaller() *Marshaller { + // We force lowerCamel JSON keys and a time layout that matches the sample payload offset (e.g. "-08"). + cfg := &config.IOConfig{ + CaseFormat: text.CaseFormatLowerCamel, + TimeLayout: "2006-01-02T15:04:05-07", + } + return New(cfg) +} + +func TestUnmarshal_SessionWithAttributes(t *testing.T) { + payload := `[{"attribute":[{"name":"theme","userId":252,"value":{"color":"dark"}}],"disabled":false,"lastSeen":"2025-11-05T17:00:07-08","userId":252}]` + + var got []Session + err := newMarshaller().Unmarshal([]byte(payload), &got) + if err != nil { + t.Fatalf("unexpected unmarshal error: %v", err) + } + if len(got) != 1 { + t.Fatalf("expected 1 session, got %d", len(got)) + } + + s := got[0] + if s.UserID != 252 { + t.Fatalf("expected userId=252, got %d", s.UserID) + } + if s.Disabled == nil || *s.Disabled != false { + t.Fatalf("expected disabled=false, got %v", s.Disabled) + } + if s.LastSeen == nil { + t.Fatalf("expected lastSeen to be set") + } + // Verify attributes + if len(s.Attribute) != 1 { + t.Fatalf("expected 1 attribute, got %d", len(s.Attribute)) + } + a := s.Attribute[0] + if a.UserID != 252 { + t.Fatalf("expected attribute.userId=252, got %d", a.UserID) + } + if a.Name == nil || *a.Name != "theme" { + if a.Name == nil { + t.Fatalf("expected attribute.name=theme, got ") + } + t.Fatalf("expected attribute.name=theme, got %s", *a.Name) + } + // Ensure raw value round-trips as expected JSON + var valueObj map[string]string + if err := stdjson.Unmarshal(a.Value, &valueObj); err != nil { + t.Fatalf("unexpected attribute.value unmarshal error: %v", err) + } + expected := map[string]string{"color": "dark"} + if !reflect.DeepEqual(valueObj, expected) { + t.Fatalf("unexpected attribute.value: got %+v want %+v", valueObj, expected) + } +} + +func TestMarshal_SessionWithAttributes(t *testing.T) { + name := "theme" + disabled := false + ts, err := time.Parse("2006-01-02T15:04:05-07", "2025-11-05T17:00:07-08") + if err != nil { + t.Fatalf("invalid test time: %v", err) + } + raw := stdjson.RawMessage(`{"color":"dark"}`) + data := []Session{ + { + UserID: 252, + LastSeen: &ts, + Disabled: &disabled, + Attribute: []Attribute{ + {UserID: 252, Name: &name, Value: raw}, + }, + }, + } + + out, err := newMarshaller().Marshal(data) + if err != nil { + t.Fatalf("unexpected marshal error: %v", err) + } + + // Compare semantically by decoding both expected and actual into generic values. + expected := `[{"attribute":[{"name":"theme","userId":252,"value":{"color":"dark"}}],"disabled":false,"lastSeen":"2025-11-05T17:00:07-08","userId":252}]` + + var gotVal, wantVal interface{} + if err := stdjson.Unmarshal(out, &gotVal); err != nil { + t.Fatalf("unexpected result json: %v, body=%s", err, string(out)) + } + if err := stdjson.Unmarshal([]byte(expected), &wantVal); err != nil { + t.Fatalf("invalid expected json: %v", err) + } + if !reflect.DeepEqual(gotVal, wantVal) { + t.Fatalf("mismatch json:\n got: %s\nwant: %s", string(out), expected) + } +} + +func TestBoolPointer_NullAndPresent(t *testing.T) { + // Case 1: disabled is null -> Disabled == nil + payloadNull := `[{"userId":1,"disabled":null}]` + var s1 []Session + if err := newMarshaller().Unmarshal([]byte(payloadNull), &s1); err != nil { + t.Fatalf("unmarshal null disabled: %v", err) + } + if len(s1) != 1 || s1[0].Disabled != nil { + t.Fatalf("expected Disabled=nil, got %+v", s1) + } + + // Case 2: disabled false -> Disabled != nil and false + payloadFalse := `[{"userId":1,"disabled":false}]` + var s2 []Session + if err := newMarshaller().Unmarshal([]byte(payloadFalse), &s2); err != nil { + t.Fatalf("unmarshal false disabled: %v", err) + } + if len(s2) != 1 || s2[0].Disabled == nil || *s2[0].Disabled != false { + t.Fatalf("expected Disabled=false pointer, got %+v", s2) + } + + // Case 3: marshal with Disabled=nil -> emits null + data := []Session{{UserID: 3}} + out, err := newMarshaller().Marshal(data) + if err != nil { + t.Fatalf("marshal nil disabled: %v", err) + } + // verify null present for disabled if not omitted by config + var v []map[string]interface{} + if err := stdjson.Unmarshal(out, &v); err != nil { + t.Fatalf("decode marshalled: %v", err) + } + if _, ok := v[0]["disabled"]; !ok { + t.Fatalf("expected disabled key present; got %s", string(out)) + } + if v[0]["disabled"] != nil { + t.Fatalf("expected disabled=null, got %v", v[0]["disabled"]) + } +} diff --git a/gateway/router/marshal/json/option.go b/gateway/router/marshal/json/option.go index cd1855380..82a8e2dd1 100644 --- a/gateway/router/marshal/json/option.go +++ b/gateway/router/marshal/json/option.go @@ -26,6 +26,6 @@ func (o Options) FormatTag() *format.Tag { } type cacheConfig struct { - ignoreCustomUnmarshaller bool - ignoreCustomMarshaller bool + IgnoreCustomUnmarshaller bool + IgnoreCustomMarshaller bool } diff --git a/gateway/router/status/error.go b/gateway/router/status/error.go index 3d9111abc..41087fe5e 100644 --- a/gateway/router/status/error.go +++ b/gateway/router/status/error.go @@ -1,9 +1,12 @@ package status import ( + "errors" + "net/http" + "github.com/viant/datly/service/executor/expand" + derrors "github.com/viant/datly/utils/errors" "github.com/viant/datly/utils/httputils" - "github.com/viant/datly/utils/types" "github.com/viant/govalidator" svalidator "github.com/viant/sqlx/io/validator" "github.com/viant/xdatly/handler/response" @@ -13,31 +16,103 @@ func NormalizeErr(err error, statusCode int) (int, string, interface{}) { violations := httputils.Violations{} switch actual := err.(type) { case *response.Error: - return actual.StatusCode(), actual.Message, nil + if derrors.IsDatabaseError(actual.Err) || derrors.IsDatabaseError(errors.New(actual.Message)) { + actual.Code = http.StatusInternalServerError + actual.Message = http.StatusText(http.StatusInternalServerError) + return http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), nil + } + code := actual.StatusCode() + if code == 0 { + code = statusCode + } + if code == 0 { + code = http.StatusBadRequest + } + // For explicit 5xx we keep response generic, for 4xx we trust the configured message. + if code >= http.StatusInternalServerError { + return code, http.StatusText(http.StatusInternalServerError), nil + } + return code, actual.Message, nil case *svalidator.Validation: ret := violations.MergeSqlViolation(actual.Violations) - return statusCode, err.Error(), ret + return http.StatusBadRequest, err.Error(), ret case *govalidator.Validation: ret := violations.MergeGoViolation(actual.Violations) - return statusCode, actual.Error(), ret + return http.StatusBadRequest, actual.Error(), ret case *response.Errors: - actual.SetStatusCode(statusCode) + maxStatus := actual.StatusCode() + if maxStatus == 0 { + maxStatus = statusCode + } + if maxStatus == 0 { + maxStatus = http.StatusBadRequest + } + hasServerError := maxStatus >= http.StatusInternalServerError || derrors.IsDatabaseError(errors.New(actual.Message)) + for _, anError := range actual.Errors { - isObj := types.IsObject(anError.Err) - if isObj { - statusCode, anError.Message, anError.Object = NormalizeErr(anError.Err, statusCode) - } else { - statusCode, anError.Message, anError.Object = NormalizeErr(anError.Err, statusCode) + if derrors.IsDatabaseError(anError.Err) || derrors.IsDatabaseError(errors.New(anError.Message)) { + anError.Code = http.StatusInternalServerError + anError.Message = http.StatusText(http.StatusInternalServerError) + hasServerError = true + } + + code := anError.StatusCode() + switch { + case code >= http.StatusInternalServerError: + anError.Message = http.StatusText(http.StatusInternalServerError) + hasServerError = true + case code == 0: + innerStatus, innerMsg, innerObj := NormalizeErr(anError.Err, maxStatus) + code = innerStatus + anError.Code = innerStatus + if innerMsg != "" { + anError.Message = innerMsg + } + if innerObj != nil { + anError.Object = innerObj + } + if code >= http.StatusInternalServerError { + hasServerError = true + } + default: + if code >= http.StatusInternalServerError { + hasServerError = true + } } + + if code > maxStatus { + maxStatus = code + } + } + + if hasServerError { + actual.Message = http.StatusText(http.StatusInternalServerError) + } else if actual.Message == "" && len(actual.Errors) > 0 { + actual.Message = actual.Errors[0].Message + } + + if maxStatus == 0 { + maxStatus = http.StatusBadRequest } - actual.SetStatusCode(statusCode) - return actual.StatusCode(), actual.Message, actual.Errors + + return maxStatus, actual.Message, actual.Errors case *expand.ErrorResponse: if actual.StatusCode != 0 { statusCode = actual.StatusCode } + // If no status code was set on the error response, treat it as a client error. + if statusCode == 0 { + statusCode = http.StatusBadRequest + } return statusCode, actual.Message, actual.Content default: + // Only DB-caused errors are mapped to 500 with a generic message. + if derrors.IsDatabaseError(err) { + return http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), nil + } + if statusCode == 0 { + statusCode = http.StatusBadRequest + } return statusCode, err.Error(), nil } } diff --git a/gateway/runtime/apigw/deploy.yaml b/gateway/runtime/apigw/deploy.yaml index 5a7d85dc6..0511616b0 100644 --- a/gateway/runtime/apigw/deploy.yaml +++ b/gateway/runtime/apigw/deploy.yaml @@ -25,7 +25,7 @@ pipeline: set_sdk: action: sdk.set target: $target - sdk: go:1.21 + sdk: go:1.25.1 build: package: diff --git a/gateway/runtime/gcr/deploy.yaml b/gateway/runtime/gcr/deploy.yaml index 0257ecf68..7cc921e0d 100644 --- a/gateway/runtime/gcr/deploy.yaml +++ b/gateway/runtime/gcr/deploy.yaml @@ -18,7 +18,7 @@ pipeline: setSdk: action: sdk.set target: $target - sdk: go:1.21 + sdk: go:1.25.1 deploy: buildBinary: diff --git a/gateway/runtime/lambda/deploy.yaml b/gateway/runtime/lambda/deploy.yaml index 6a7fbae63..0ea83df8d 100644 --- a/gateway/runtime/lambda/deploy.yaml +++ b/gateway/runtime/lambda/deploy.yaml @@ -27,7 +27,7 @@ pipeline: set_sdk: action: sdk.set target: $target - sdk: go:1.21 + sdk: go:1.25.1 build: package: diff --git a/go.mod b/go.mod index 89bb2f2dd..dc81b120e 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,12 @@ module github.com/viant/datly -go 1.23.8 +go 1.25.0 require ( github.com/aerospike/aerospike-client-go v4.5.2+incompatible github.com/aws/aws-lambda-go v1.31.0 github.com/francoispqt/gojay v1.2.13 github.com/go-sql-driver/mysql v1.7.0 - github.com/goccy/go-json v0.10.5 github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/google/gops v0.3.23 github.com/google/uuid v1.6.0 @@ -15,9 +14,9 @@ require ( github.com/lib/pq v1.10.6 github.com/mattn/go-sqlite3 v1.14.16 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/viant/afs v1.26.2 - github.com/viant/afsc v1.9.1 + github.com/viant/afsc v1.16.0 github.com/viant/assertly v0.9.1-0.20220620174148-bab013f93a60 github.com/viant/bigquery v0.4.1 github.com/viant/cloudless v1.12.0 @@ -26,93 +25,111 @@ require ( github.com/viant/dyndb v0.1.4-0.20221214043424-27654ab6ed9c github.com/viant/gmetric v0.3.2 github.com/viant/godiff v0.4.1 - github.com/viant/parsly v0.3.3-0.20240717150634-e1afaedb691b + github.com/viant/parsly v0.3.3 github.com/viant/pgo v0.11.0 github.com/viant/scy v0.24.0 - github.com/viant/sqlx v0.17.6 - github.com/viant/structql v0.5.2 + github.com/viant/sqlx v0.21.0 + github.com/viant/structql v0.5.4 github.com/viant/toolbox v0.37.0 github.com/viant/velty v0.2.1-0.20230927172116-ba56497b5c85 github.com/viant/xreflect v0.7.3 github.com/viant/xunsafe v0.10.3 - golang.org/x/mod v0.25.0 - golang.org/x/oauth2 v0.30.0 - google.golang.org/api v0.174.0 + golang.org/x/mod v0.28.0 + golang.org/x/oauth2 v0.32.0 + google.golang.org/api v0.201.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/viant/govalidator v0.3.1 - github.com/viant/sqlparser v0.8.1 + github.com/viant/sqlparser v0.9.0 ) require ( github.com/viant/aerospike v0.2.11-0.20241108195857-ed524b97800d github.com/viant/firebase v0.1.1 - github.com/viant/jsonrpc v0.7.2 - github.com/viant/mcp v0.4.3 - github.com/viant/mcp-protocol v0.4.4 - github.com/viant/structology v0.6.1 - github.com/viant/tagly v0.2.2 - github.com/viant/xdatly v0.5.4-0.20250806192028-819cadf93282 + github.com/viant/jsonrpc v0.15.0 + github.com/viant/mcp v0.8.0 + github.com/viant/mcp-protocol v0.5.10 + github.com/viant/structology v0.8.0 + github.com/viant/tagly v0.3.0 + github.com/viant/xdatly v0.5.4-0.20251113181159-0ac8b8b0ff3a github.com/viant/xdatly/extension v0.0.0-20231013204918-ecf3c2edf259 - github.com/viant/xdatly/handler v0.0.0-20250806192028-819cadf93282 + github.com/viant/xdatly/handler v0.0.0-20251208172928-dd34b7f09fd5 github.com/viant/xdatly/types/core v0.0.0-20250307183722-8c84fc717b52 github.com/viant/xdatly/types/custom v0.0.0-20240801144911-4c2bfca4c23a github.com/viant/xlsy v0.3.1 github.com/viant/xmlify v0.1.1 - golang.org/x/net v0.40.0 - golang.org/x/tools v0.33.0 + golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 + golang.org/x/tools v0.37.0 modernc.org/sqlite v1.18.1 ) require ( - cloud.google.com/go v0.112.1 // indirect - cloud.google.com/go/auth v0.2.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.0 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect - cloud.google.com/go/firestore v1.15.0 // indirect - cloud.google.com/go/iam v1.1.7 // indirect - cloud.google.com/go/longrunning v0.5.5 // indirect - cloud.google.com/go/secretmanager v1.11.5 // indirect - cloud.google.com/go/storage v1.40.0 // indirect + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go v0.116.0 // indirect + cloud.google.com/go/auth v0.9.8 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/firestore v1.17.0 // indirect + cloud.google.com/go/iam v1.2.1 // indirect + cloud.google.com/go/longrunning v0.6.1 // indirect + cloud.google.com/go/monitoring v1.21.1 // indirect + cloud.google.com/go/secretmanager v1.14.1 // indirect + cloud.google.com/go/storage v1.45.0 // indirect firebase.google.com/go v3.13.0+incompatible // indirect firebase.google.com/go/v4 v4.14.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/aerospike/aerospike-client-go/v6 v6.15.1 // indirect github.com/aws/aws-sdk-go v1.51.23 // indirect - github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect - github.com/aws/aws-sdk-go-v2/config v1.27.11 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.26 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.7 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.8 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.27 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.20 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 // indirect github.com/aws/aws-sdk-go-v2/service/sns v1.31.3 // indirect github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.22.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect - github.com/aws/smithy-go v1.20.3 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssm v1.55.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect + github.com/aws/smithy-go v1.22.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-errors/errors v1.5.1 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect @@ -125,10 +142,13 @@ require ( github.com/mazznoer/csscolorparser v0.1.3 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/nxadm/tail v1.4.8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/viant/gosh v0.2.1 // indirect github.com/viant/igo v0.2.0 // indirect github.com/viant/x v0.3.0 // indirect github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca // indirect @@ -136,24 +156,28 @@ require ( github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a // indirect github.com/yuin/gopher-lua v1.1.1 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 // indirect - golang.org/x/time v0.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.7.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine/v2 v2.0.2 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect modernc.org/cc/v3 v3.36.3 // indirect diff --git a/go.sum b/go.sum index 736b0b41e..aafc7c00c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -39,8 +41,8 @@ cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFO cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -102,10 +104,10 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/auth v0.2.0 h1:y6oTcpMSbOcXbwYgUUrvI+mrQ2xbrcdpPgtVbCGTLTk= -cloud.google.com/go/auth v0.2.0/go.mod h1:+yb+oy3/P0geX6DLKlqiGHARGR6EX2GRtYCzWOCQSbU= -cloud.google.com/go/auth/oauth2adapt v0.2.0 h1:FR8zevgQwu+8CqiOT5r6xCmJa3pJC/wdXEEPF1OkNhA= -cloud.google.com/go/auth/oauth2adapt v0.2.0/go.mod h1:AfqujpDAlTfLfeCIl/HJZZlIxD8+nJoZ5e0x1IxGq5k= +cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8= +cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= @@ -186,8 +188,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -283,8 +285,8 @@ cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLY cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= -cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= +cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ= +cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y= cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= @@ -323,8 +325,8 @@ cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGE cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= -cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= +cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= +cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= @@ -354,11 +356,13 @@ cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6 cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/logging v1.11.0 h1:v3ktVzXMV7CwHq1MBF65wcqLMA7i+z3YxbUsoK7mOKs= +cloud.google.com/go/logging v1.11.0/go.mod h1:5LDiJC/RxTt+fHc1LAt20R9TKiUTReDg6RuuFOZ67+A= cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= -cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= +cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= +cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= @@ -382,6 +386,8 @@ cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhI cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc= +cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= @@ -490,8 +496,8 @@ cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISI cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/secretmanager v1.11.5 h1:82fpF5vBBvu9XW4qj0FU2C6qVMtj1RM/XHwKXUEAfYY= -cloud.google.com/go/secretmanager v1.11.5/go.mod h1:eAGv+DaCHkeVyQi0BeXgAHOU0RdrMeZIASKc+S7VqH4= +cloud.google.com/go/secretmanager v1.14.1 h1:xlWSIg8rtBn5qCr2f3XtQP19+5COyf/ll49SEvi/0vM= +cloud.google.com/go/secretmanager v1.14.1/go.mod h1:L+gO+u2JA9CCyXpSR8gDH0o8EV7i/f0jdBOrUXcIV0U= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= @@ -547,8 +553,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= -cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= +cloud.google.com/go/storage v1.45.0 h1:5av0QcIVj77t+44mV4gffFC/LscFRUhto6UBMB5SimM= +cloud.google.com/go/storage v1.45.0/go.mod h1:wpPblkIuMP5jCB/E48Pz9zIo2S/zD8g+ITmxKkPCITE= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= @@ -568,6 +574,8 @@ cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/trace v1.11.1 h1:UNqdP+HYYtnm6lb91aNA5JQ0X14GnxkABGlfz2PzPew= +cloud.google.com/go/trace v1.11.1/go.mod h1:IQKNQuBzH72EGaXEodKlNJrWykGZxet2zgjtS60OtjA= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= @@ -628,6 +636,14 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1/go.mod h1:0wEl7vrAD8mehJyohS9HZy+WyEOaQO2mJx86Cvh93kM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= @@ -652,48 +668,64 @@ github.com/aws/aws-lambda-go v1.31.0/go.mod h1:IF5Q7wj4VyZyUFnZ54IQqeWtctHQ9tz+K github.com/aws/aws-sdk-go v1.51.23 h1:/3TEdsEE/aHmdKGw2NrOp7Sdea76zfffGkTTSXTsDxY= github.com/aws/aws-sdk-go v1.51.23/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.17.2/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= -github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= -github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA= -github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE= -github.com/aws/aws-sdk-go-v2/credentials v1.17.26 h1:tsm8g/nJxi8+/7XyJJcP2dLrnK/5rkFp6+i2nhmz5fk= -github.com/aws/aws-sdk-go-v2/credentials v1.17.26/go.mod h1:3vAM49zkIa3q8WT6o9Ve5Z0vdByDMwmdScO0zvThTgI= +github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= +github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA= +github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ= +github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.7 h1:CyuByiiCA4lPfU8RaHJh2wIYYn0hkFlOkMfWkVY67Mc= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.7/go.mod h1:pAMtgCPVxcKohC/HNI6nLwLeW007eYl3T+pq7yTMV3o= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33 h1:X+4YY5kZRI/cOoSMVMGTqFXHAMg1bvvay7IBcqHpybQ= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33/go.mod h1:DPynzu+cn92k5UQ6tZhX+wfTB4ah6QDU/NgdHqatmvk= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.26/go.mod h1:2E0LdbJW6lbeU4uxjum99GZzI0ZjDpAb0CoSCM0oeEY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.20/go.mod h1:/+6lSiby8TBFpTVXZgKiN/rCfkYXEGvhlM4zCgPpt7w= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 h1:7edmS3VOBDhK00b/MwGtGglCm7hhwNYnjJs/PgFdMQE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21/go.mod h1:Q9o5h4HoIWG8XfzxqiuK/CGUbepCJ8uTlaE3bAbxytQ= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.8 h1:VgdGaSIoH4JhUZIspT8UgK0aBF85TiLve7VHEx3NfqE= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.8/go.mod h1:jvXzk+hVrlkiQOvnq6jH+F6qBK0CEceXkEWugT+4Kdc= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.27 h1:7MhqbR+k+b0gbOxp+W8yXgsl/Z5/dtMh85K0WI8X2EA= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.27/go.mod h1:wX9QEZJ8Dw1fdAKCOAUmSvAe3wNJFxnE/4AeYc8blGA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 h1:4FMHqLfk0efmTqhXVRL5xYRqlEBNBiRI7N6w4jsEdd4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2/go.mod h1:LWoqeWlK9OZeJxsROW2RqrSPvQHKTpp69r/iDjwsSaw= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.20 h1:kSZR22oLBDMtP8ZPGXhz649NU77xsJDG7g3xfT6nHVk= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.20/go.mod h1:lxM5qubwGNX29Qy+xTFG8G0r2Mj/TmyC+h3hS/7E4V8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 h1:t7iUP9+4wdc5lt3E41huP+GvQZJD38WLsgVp4iOtAjg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2/go.mod h1:/niFCtmuQNxqx9v8WAPq5qh7EH25U4BF6tjoyq9bObM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0 h1:xA6XhTF7PE89BCNHJbQi8VvPzcgMtmGC5dr8S8N7lHk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0/go.mod h1:cB6oAuus7YXRZhWCc1wIwPywwZ1XwweNp2TVAEGYeB8= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 h1:Rrqru2wYkKQCS2IM5/JrgKUQIoNTqA6y/iuxkjzxC6M= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2/go.mod h1:QuCURO98Sqee2AXmqDNxKXYFm2OEDAVAPApMqO0Vqnc= github.com/aws/aws-sdk-go-v2/service/sns v1.31.3 h1:eSTEdxkfle2G98FE+Xl3db/XAXXVTJPNQo9K/Ar8oAI= github.com/aws/aws-sdk-go-v2/service/sns v1.31.3/go.mod h1:1dn0delSO3J69THuty5iwP0US2Glt0mx2qBBlI13pvw= github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3 h1:Vjqy5BZCOIsn4Pj8xzyqgGmsSqzz7y/WXbN3RgOoVrc= github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3/go.mod h1:L0enV3GCRd5iG9B64W35C4/hwsCB00Ib+DKVGTadKHI= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.3 h1:Fv1vD2L65Jnp5QRsdiM64JvUM4Xe+E0JyVsRQKv6IeA= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.3/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= +github.com/aws/aws-sdk-go-v2/service/ssm v1.55.2 h1:z6Pq4+jtKlhK4wWJGHRGwMLGjC1HZwAO3KJr/Na0tSU= +github.com/aws/aws-sdk-go-v2/service/ssm v1.55.2/go.mod h1:DSmu/VZzpQlAubWBbAvNpt+S4k/XweglJi4XaDGyvQk= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= -github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -705,6 +737,8 @@ github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -722,11 +756,14 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= @@ -744,10 +781,18 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -771,11 +816,13 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -786,12 +833,9 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= @@ -857,8 +901,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gops v0.3.23 h1:OjsHRINl5FiIyTc8jivIg4UN0GY6Nh32SL8KRbl8GQo= @@ -868,8 +912,9 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -891,8 +936,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM= github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -902,8 +947,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -920,8 +965,8 @@ github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38 github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -957,8 +1002,9 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -1022,8 +1068,11 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -1041,8 +1090,9 @@ github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTK github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= @@ -1076,6 +1126,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -1090,8 +1142,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= @@ -1099,8 +1151,8 @@ github.com/viant/aerospike v0.2.11-0.20241108195857-ed524b97800d h1:IRmoMmrWqkHD github.com/viant/aerospike v0.2.11-0.20241108195857-ed524b97800d/go.mod h1:eRBywl0oTDM/oGhGLUeJjnC7XzmkTGuW9/og5YFy0K0= github.com/viant/afs v1.26.2 h1:rOs/iFxFlEndhIRATJVXlNWhVU0cGdRQAGVTVJPdsc0= github.com/viant/afs v1.26.2/go.mod h1:rScbFd9LJPGTM8HOI8Kjwee0AZ+MZMupAvFpPg+Qdj4= -github.com/viant/afsc v1.9.1 h1:BIus7fYyjM+MDgKuAzCBfoV4oVy2xTVhuFsQKUCPvkQ= -github.com/viant/afsc v1.9.1/go.mod h1:FA/xVjaMM10qGByabP8anTVMH6N4eUsAeWm5xcEZJJA= +github.com/viant/afsc v1.16.0 h1:/kOH/flNwme6h3oFrU/KPnMHkhbCZxQncTf1GSQIlBQ= +github.com/viant/afsc v1.16.0/go.mod h1:Z6fP3VcmzS8Sg2lowctR6KkVEX7XxJ8aNaoHqhUiZkY= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/assertly v0.9.0/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/assertly v0.9.1-0.20220620174148-bab013f93a60 h1:VFJvCOHKXv4IqX8rJwn1otpHWQGgMDv2bXtAPgEzndM= @@ -1127,30 +1179,28 @@ github.com/viant/govalidator v0.3.1 h1:V7f/KgfzbP8fVDc+Kj+jyPvfXxMr2N1x7srOlDV6l github.com/viant/govalidator v0.3.1/go.mod h1:D35Dwx0R8rR1knRxhlseoYvOkiqo24kpMg1/o977i9Y= github.com/viant/igo v0.2.0 h1:ygWmTCinnGPaeV7omJLiyneOpzYZ5kiw7oYz7mUJZVQ= github.com/viant/igo v0.2.0/go.mod h1:7V6AWsLhKWeGzXNTNH3AZiIEKa0m33DrQbdWtapsI74= -github.com/viant/jsonrpc v0.7.2 h1:FUzhfFN76E09ZbQOxReFOyPhsxYhE0fjWzPhattR9Dk= -github.com/viant/jsonrpc v0.7.2/go.mod h1:LW2l5/H4KkGCsx2ktPX59iUlycw85ZlBcRuK/WYWBX8= -github.com/viant/mcp v0.4.3 h1:ykQ2XyS2l5xrxHY5peJgIWoH+n8ZSpiSifnO/UH6/3I= -github.com/viant/mcp v0.4.3/go.mod h1:3SnILtYVIT8PIWICMyzP9KfhepawoFRv+//FBU/hc7c= -github.com/viant/mcp-protocol v0.4.4 h1:jKuCHvXeNof1Of1UfUyJkrSSNfOBiN4pXKWv3J2NwFM= -github.com/viant/mcp-protocol v0.4.4/go.mod h1:EL4NY7yW2gge+XLorgJA7PIazQX3x4ZkutYihwBwINs= -github.com/viant/parsly v0.3.3-0.20240717150634-e1afaedb691b h1:3q166tV28yFdbFV+tXXjH7ViKAmgAgGdoWzMtvhQv28= -github.com/viant/parsly v0.3.3-0.20240717150634-e1afaedb691b/go.mod h1:85fneXJbErKMGhSQto3A5ElTQCwl3t74U9cSV0waBHw= +github.com/viant/jsonrpc v0.15.0 h1:0qy9vzgNwR9Gj1C+ouSrzNUtNDzKGogO+7TZR+cFrA4= +github.com/viant/jsonrpc v0.15.0/go.mod h1:b214Lo4zBwLqbu6Tf2bRlgQkFfPMBW5ap4qS+I3zcJ8= +github.com/viant/mcp v0.8.0 h1:n4tnLXpOtpnrLZtHyNG2mmZ9SUbGWKsWGla10iMfuDg= +github.com/viant/mcp v0.8.0/go.mod h1:fyuB1TSQYbbGNn7U6rLmlr9gD+Yg5+Na32D34Uvm0sk= +github.com/viant/mcp-protocol v0.5.10 h1:915EC1GKgBbyYF4efzRSZ/AE6f4vobkbwa2qe6OOjJ0= +github.com/viant/mcp-protocol v0.5.10/go.mod h1:EJPomVw6jnI+4Aa2ONYC3WTvApiF0YeQIiaaEpA54ec= +github.com/viant/parsly v0.3.3 h1:7ytgfLOG4Ils+wviGacWxRD0gAUvVEH/iGsSE+UI8YM= +github.com/viant/parsly v0.3.3/go.mod h1:85fneXJbErKMGhSQto3A5ElTQCwl3t74U9cSV0waBHw= github.com/viant/pgo v0.11.0 h1:PNuYVhwTfyrAHGBO6lxaMFuHP4NkjKV8ULecz3OWk8c= github.com/viant/pgo v0.11.0/go.mod h1:MFzHmkRFZlciugEgUvpl/3grK789PBSH4dUVSLOSo+Q= github.com/viant/scy v0.24.0 h1:KAC3IUARkQxTNSuwBK2YhVBJMOOLN30YaLKHbbuSkMU= github.com/viant/scy v0.24.0/go.mod h1:7uNRS67X45YN+JqTLCcMEhehffVjqrejULEDln9p0Ao= -github.com/viant/sqlparser v0.8.1 h1:nbcTecMtW7ROk5aNB5/BWUxnduepRPOkhVo9RWxI1Ns= -github.com/viant/sqlparser v0.8.1/go.mod h1:2QRGiGZYk2/pjhORGG1zLVQ9JO+bXFhqIVi31mkCRPg= -github.com/viant/sqlx v0.16.6 h1:3/D1/c3E8cMaUWTUBW56Gg/1vW4QMMWm42HkSAbzSZQ= -github.com/viant/sqlx v0.16.6/go.mod h1:dizufL+nTNqDCpivUnE2HqtddTp2TdA6WFghGfZo11c= -github.com/viant/sqlx v0.17.6 h1:6uMZVWk+WJl/y8coEh4F4mqbTHbtzWkLVEQdrk+m7sE= -github.com/viant/sqlx v0.17.6/go.mod h1:dizufL+nTNqDCpivUnE2HqtddTp2TdA6WFghGfZo11c= -github.com/viant/structology v0.6.1 h1:Forza+RF/1tmlQFk9ABNhu+IQ8vMAqbYM6FOsYtGh9E= -github.com/viant/structology v0.6.1/go.mod h1:63XfkzUyNw7wdi99HJIsH2Rg3d5AOumqbWLUYytOkxU= -github.com/viant/structql v0.5.2 h1:0dAratszxC6AD/TNaV8BnLQQprNO5GJHaKjmszrIoeY= -github.com/viant/structql v0.5.2/go.mod h1:nm9AYnAuSKH7b7pG+dKVxbQrr1Mgp1CQEMvUwwkE+I8= -github.com/viant/tagly v0.2.2 h1:qqb4Dov83i7nl7Gewph/lLaYAF8MKv0N7y34scgRNmE= -github.com/viant/tagly v0.2.2/go.mod h1:vV8QgJkhug+X+qyKds8av0fhjD+4u7IhNtowL1KGQ5A= +github.com/viant/sqlparser v0.9.0 h1:MoRJ18cm4MeSGLMNO8jZZzb1S5rLaIksEbdqE+8RBEw= +github.com/viant/sqlparser v0.9.0/go.mod h1:2QRGiGZYk2/pjhORGG1zLVQ9JO+bXFhqIVi31mkCRPg= +github.com/viant/sqlx v0.21.0 h1:Lx5KXmzfSjSvZZX5P0Ua9kFGvAmCxAjLOPe9pQA7VmY= +github.com/viant/sqlx v0.21.0/go.mod h1:woTOwNiqvt6SqkI+5nyzlixcRTTV0IvLZUTberqb8mo= +github.com/viant/structology v0.8.0 h1:WKdK67l+O1eqsubn8PWMhWcgspUGJ22SgJxUMfiRgqE= +github.com/viant/structology v0.8.0/go.mod h1:Fnm1DyR4gfyPbnhBMkQB5lR6/isYDnncBFO1nCxxmqs= +github.com/viant/structql v0.5.4 h1:bMdcOpzU8UMoe5OBcyJVRxLAndvU1oj3ysvPUgBckCI= +github.com/viant/structql v0.5.4/go.mod h1:nm9AYnAuSKH7b7pG+dKVxbQrr1Mgp1CQEMvUwwkE+I8= +github.com/viant/tagly v0.3.0 h1:Y8IckveeSrroR8yisq4MBdxhcNqf4v8II01uCpamh4E= +github.com/viant/tagly v0.3.0/go.mod h1:PauQQkHmAvL5lFGr4gIgi+PE0aUPggBIBYN34sX2Oes= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/viant/toolbox v0.34.5/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/viant/toolbox v0.37.0 h1:+zwSdbQh6I6ZEyxokQJr+1gQKbLEw6erc+Av5dwKtLU= @@ -1159,12 +1209,12 @@ github.com/viant/velty v0.2.1-0.20230927172116-ba56497b5c85 h1:zKk+6hqUipkJXCPCH github.com/viant/velty v0.2.1-0.20230927172116-ba56497b5c85/go.mod h1:Q/UXviI2Nli8WROEpYd/BELMCSvnulQeyNrbPmMiS/Y= github.com/viant/x v0.3.0 h1:/3A0z/uySGxMo6ixH90VAcdjI00w5e3REC1zg5hzhJA= github.com/viant/x v0.3.0/go.mod h1:54jP3qV+nnQdNDaWxEwGTAAzCu9sx9er9htiwTW/Mcw= -github.com/viant/xdatly v0.5.4-0.20250806192028-819cadf93282 h1:CqRQGsior7arN1lQA11oCoWdC/LZv1ObhCOGpdwvR3k= -github.com/viant/xdatly v0.5.4-0.20250806192028-819cadf93282/go.mod h1:lZKZHhVdCZ3U9TU6GUFxKoGN3dPtqt2HkDYzJPq5CEs= +github.com/viant/xdatly v0.5.4-0.20251113181159-0ac8b8b0ff3a h1:7CLO2LjVnFgOwN0FL3Q4y5NrD7DpclS21AiW6tDLIc8= +github.com/viant/xdatly v0.5.4-0.20251113181159-0ac8b8b0ff3a/go.mod h1:lZKZHhVdCZ3U9TU6GUFxKoGN3dPtqt2HkDYzJPq5CEs= github.com/viant/xdatly/extension v0.0.0-20231013204918-ecf3c2edf259 h1:9Yry3PUBDzc4rWacOYvAq/TKrTV0agvMF0gwm2gaoHI= github.com/viant/xdatly/extension v0.0.0-20231013204918-ecf3c2edf259/go.mod h1:fb8YgbVadk8X5ZLz49LWGzWmQlZd7Y/I5wE0ru44bIo= -github.com/viant/xdatly/handler v0.0.0-20250806192028-819cadf93282 h1:oNhkNyC6bRBifxWLyd7MTEFmCMwfg1LaAjKAmubrWCM= -github.com/viant/xdatly/handler v0.0.0-20250806192028-819cadf93282/go.mod h1:OeV4sVatklNs31nFnZtSp7lEvKJRoVJbH5opNRmRPg0= +github.com/viant/xdatly/handler v0.0.0-20251208172928-dd34b7f09fd5 h1:CrT0HTlQul8FoGN0peylVczAOUEXKVqRAiB35ypRNHY= +github.com/viant/xdatly/handler v0.0.0-20251208172928-dd34b7f09fd5/go.mod h1:OeV4sVatklNs31nFnZtSp7lEvKJRoVJbH5opNRmRPg0= github.com/viant/xdatly/types/core v0.0.0-20250307183722-8c84fc717b52 h1:G+e1MMDxQXUPPlAXVNlRqSLTLra7udGQZUu3hnr0Y8M= github.com/viant/xdatly/types/core v0.0.0-20250307183722-8c84fc717b52/go.mod h1:LJN2m8xJjtYNCvyvNrVanJwvzj8+hYCuPswL8H4qRG0= github.com/viant/xdatly/types/custom v0.0.0-20240801144911-4c2bfca4c23a h1:jecH7mH63gj1zJwD18SdvSHM9Ttr9FEOnhHkYfkCNkI= @@ -1205,18 +1255,24 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -1242,8 +1298,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1306,8 +1362,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1377,8 +1433,8 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo= +golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1412,8 +1468,8 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1433,8 +1489,8 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1526,8 +1582,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1543,8 +1599,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1564,8 +1620,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1573,8 +1629,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1639,8 +1695,8 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1649,12 +1705,12 @@ golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= @@ -1723,8 +1779,8 @@ google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjY google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= -google.golang.org/api v0.174.0 h1:zB1BWl7ocxfTea2aQ9mgdzXjnfPySllpPOskdnO+q34= -google.golang.org/api v0.174.0/go.mod h1:aC7tB6j0HR1Nl0ni5ghpx6iLasmAX78Zkh/wgxAAjLg= +google.golang.org/api v0.201.0 h1:+7AD9JNM3tREtawRMu8sOjSbb8VYcYXJG/2eEOmfDu0= +google.golang.org/api v0.201.0/go.mod h1:HVY0FCHVs89xIW9fzf/pBvOEm+OolHa86G/txFezyq4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1877,21 +1933,21 @@ google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc= -google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -1936,8 +1992,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1957,8 +2013,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/codegen/handler.go b/internal/codegen/handler.go index 5c732a16e..dafbd99a8 100644 --- a/internal/codegen/handler.go +++ b/internal/codegen/handler.go @@ -2,11 +2,12 @@ package codegen import ( _ "embed" + "strings" + "github.com/viant/datly/cmd/options" "github.com/viant/datly/internal/codegen/ast" "github.com/viant/datly/internal/inference" "github.com/viant/datly/internal/plugin" - "strings" ) //go:embed tmpl/handler/handler.gox diff --git a/internal/inference/parameter.go b/internal/inference/parameter.go index d3403859f..9ab895711 100644 --- a/internal/inference/parameter.go +++ b/internal/inference/parameter.go @@ -4,6 +4,12 @@ import ( "embed" _ "embed" "fmt" + "go/ast" + "path" + "reflect" + "strconv" + "strings" + "github.com/viant/datly/view" "github.com/viant/datly/view/state" "github.com/viant/datly/view/tags" @@ -15,11 +21,6 @@ import ( "github.com/viant/tagly/format/text" "github.com/viant/xreflect" "github.com/viant/xunsafe" - "go/ast" - "path" - "reflect" - "strconv" - "strings" ) type ( @@ -34,6 +35,7 @@ type ( AssumedType bool Connector string Cache string + Limit *int InOutput bool Of string } @@ -78,18 +80,19 @@ func (p *Parameter) veltyDeclaration(builder *strings.Builder) { case state.KindParam: builder.WriteString("?") default: + isPtr := strings.HasPrefix(p.Schema.DataType, "*") if p.Schema.Cardinality == state.Many { builder.WriteString("[]") - switch p.In.Kind { case "query", "form", "header": default: - if !p.IsRequired() { + if !p.IsRequired() && !isPtr { + isPtr = true builder.WriteString("*") } } - } else if !p.IsRequired() { + } else if !p.IsRequired() && !isPtr { builder.WriteString("*") } builder.WriteString(p.Schema.DataType) @@ -115,6 +118,10 @@ func (p *Parameter) veltyDeclaration(builder *strings.Builder) { builder.WriteString(".WithCache('" + p.Cache + "')") } + if p.Limit != nil { + builder.WriteString(".WithLimit('" + strconv.Itoa(*p.Limit) + "')") + } + if p.Required != nil { if !*p.Required { builder.WriteString(".Optional()") @@ -122,6 +129,10 @@ func (p *Parameter) veltyDeclaration(builder *strings.Builder) { builder.WriteString(".Required()") } } + + if p.Cacheable != nil { + builder.WriteString(".Cacheable('" + strconv.FormatBool(*p.Cacheable) + "')") + } if p.Connector != "" { builder.WriteString(".WithConnector('" + p.Connector + "')") } @@ -304,6 +315,9 @@ func buildParameter(field *xunsafe.Field, aTag *tags.Tag, types *xreflect.Types, if aTag.View.Cache != "" { param.Cache = aTag.View.Cache } + if aTag.View.Limit != nil { + param.Limit = aTag.View.Limit + } } fType := field.Type diff --git a/internal/inference/state.go b/internal/inference/state.go index e00c2b3b0..e309bc01f 100644 --- a/internal/inference/state.go +++ b/internal/inference/state.go @@ -3,6 +3,12 @@ package inference import ( "context" "fmt" + "go/ast" + "go/parser" + "path" + "reflect" + "strings" + "github.com/viant/afs" "github.com/viant/afs/embed" "github.com/viant/afs/file" @@ -19,11 +25,6 @@ import ( "github.com/viant/toolbox/data" "github.com/viant/xreflect" "github.com/viant/xunsafe" - "go/ast" - "go/parser" - "path" - "reflect" - "strings" ) // State defines datly view/resource parameters @@ -691,12 +692,20 @@ func NewState(packageLocation, dataType string, types *xreflect.Types) (State, e } state.BuildPredicate(aTag, ¶m.Parameter) state.BuildCodec(aTag, ¶m.Parameter) + if param.Schema.DataType == "" { compType := param.Schema.CompType() + paramTypeName := compType.String() + if compType.Kind() == reflect.Pointer { compType = compType.Elem() + paramTypeName := compType.String() + + if compType.Kind() == reflect.Struct { + paramTypeName = "*" + paramTypeName + } } - param.Schema.DataType = compType.String() + param.Schema.DataType = paramTypeName param.Schema.PackagePath = compType.PkgPath() } //} @@ -776,6 +785,13 @@ func discoverStateType(baseDir string, types *xreflect.Types, dataType string, p return nil, err } var rType = xunsafe.LookupType(dirTypes.ModulePath + "/" + dataType) + + if rType == nil && types != nil && strings.Count(pkg, "/") > 1 { //the last resort fallback collission protection + pkg = strings.Replace(pkg, "pkg/", "", 1) + rType, _ = types.Lookup(dataType, xreflect.WithPackage(pkg)) + + } + if rType == nil && len(stateTypeFields) > 0 { rType = reflect.StructOf(stateTypeFields) } diff --git a/internal/inference/type.go b/internal/inference/type.go index 3694aea03..3982045b4 100644 --- a/internal/inference/type.go +++ b/internal/inference/type.go @@ -229,11 +229,13 @@ func NewType(packageName string, name string, rType reflect.Type) (*Type, error) rType = types.EnsureStruct(rType) if rType.NumField() == 1 { wrapperField := rType.Field(0) - if canidateType, _ := wrapperField.Tag.Lookup("typeName"); canidateType != "" { - name = canidateType + if types.EnsureStruct(wrapperField.Type) != nil { + if canidateType, _ := wrapperField.Tag.Lookup("typeName"); canidateType != "" { + name = canidateType + } + structType := types.EnsureStruct(wrapperField.Type) + return NewType(packageName, name, structType) } - structType := types.EnsureStruct(wrapperField.Type) - return NewType(packageName, name, structType) } for i := 0; i < rType.NumField(); i++ { diff --git a/internal/translator/function/allowedorderbycolumn.go b/internal/translator/function/allowedorderbycolumn.go new file mode 100644 index 000000000..cfad0bbf7 --- /dev/null +++ b/internal/translator/function/allowedorderbycolumn.go @@ -0,0 +1,76 @@ +package function + +import ( + "fmt" + "strings" + + "github.com/viant/datly/view" + "github.com/viant/sqlparser" +) + +type allowedOrderByColumns struct{} + +func (c *allowedOrderByColumns) Apply(args []string, column *sqlparser.Column, resource *view.Resource, aView *view.View) error { + if aView.Selector == nil { + aView.Selector = &view.Config{} + } + values, err := convertArguments(c, args) + if err != nil { + return err + } + if aView.Selector.Constraints == nil { + aView.Selector.Constraints = &view.Constraints{} + } + aView.Selector.Constraints.OrderBy = true + if len(values) == 0 { + return fmt.Errorf("failed to discover expression in allowedOrderByColumns") + } + columns, ok := values[0].(string) + if !ok { + return fmt.Errorf("invalid columns type: %T, expected: %T in allowedOrderByColumns", values[0], columns) + } + if len(aView.Selector.Constraints.OrderByColumn) == 0 { + aView.Selector.Constraints.OrderByColumn = map[string]string{} + } + for _, expression := range strings.Split(columns, ",") { + expression = strings.TrimSpace(expression) + + key := expression + value := expression + if strings.Contains(expression, ":") { + parts := strings.SplitN(expression, ":", 2) + key = parts[0] + value = parts[1] + } + + aView.Selector.Constraints.OrderByColumn[key] = value + lcKey := strings.ToLower(key) + if lcKey != key { + aView.Selector.Constraints.OrderByColumn[lcKey] = value + } + + if index := strings.Index(key, "."); index != -1 { + aView.Selector.Constraints.OrderByColumn[key[index+1:]] = value + } + } + return nil +} + +func (c *allowedOrderByColumns) Name() string { + return "allowed_order_by_columns" +} + +func (c *allowedOrderByColumns) Description() string { + return "set view.Selector.OrderBy and enables corresponding view.Selector.Constraints.OrderBy" +} + +func (c *allowedOrderByColumns) Arguments() []*Argument { + return []*Argument{ + { + Name: "allowedOrderByColumns", + Description: "query selector allowedOrderByColumns", + Required: true, + DataType: "string", + }, + } +} diff --git a/internal/translator/function/init.go b/internal/translator/function/init.go index b67d258f0..b0141c3d8 100644 --- a/internal/translator/function/init.go +++ b/internal/translator/function/init.go @@ -5,6 +5,7 @@ func init() { _registry.Register(&cache{}) _registry.Register(&limit{}) _registry.Register(&orderBy{}) + _registry.Register(&allowedOrderByColumns{}) _registry.Register(&cardinality{}) _registry.Register(&allownulls{}) _registry.Register(&matchStrategy{}) diff --git a/internal/translator/function/orderby.go b/internal/translator/function/orderby.go index 645be6058..9dc22f5ca 100644 --- a/internal/translator/function/orderby.go +++ b/internal/translator/function/orderby.go @@ -20,6 +20,7 @@ func (c *orderBy) Apply(args []string, column *sqlparser.Column, resource *view. } aView.Selector.Constraints.OrderBy = true aView.Selector.OrderBy = values[0].(string) + return nil } diff --git a/internal/translator/parser/declarations.go b/internal/translator/parser/declarations.go index ca11710de..d5113539e 100644 --- a/internal/translator/parser/declarations.go +++ b/internal/translator/parser/declarations.go @@ -2,6 +2,10 @@ package parser import ( "fmt" + "reflect" + "strconv" + "strings" + "github.com/viant/datly/gateway/router/marshal" "github.com/viant/datly/internal/inference" "github.com/viant/datly/shared" @@ -12,9 +16,6 @@ import ( "github.com/viant/velty/ast/expr" "github.com/viant/velty/parser" "github.com/viant/xreflect" - "reflect" - "strconv" - "strings" ) type ( @@ -322,6 +323,10 @@ func (s *Declarations) parseShorthands(declaration *Declaration, cursor *parsly. declaration.InOutput = true case "WithCache": declaration.Cache = strings.Trim(args[0], `"'`) + case "WithLimit": + limit, _ := strconv.Atoi(strings.Trim(args[0], `"'`)) + declaration.Limit = &limit + case "Cacheable": literal := strings.Trim(args[0], `"'`) value, _ := strconv.ParseBool(literal) diff --git a/internal/translator/parser/statement.go b/internal/translator/parser/statement.go index 3874f4c48..39633964f 100644 --- a/internal/translator/parser/statement.go +++ b/internal/translator/parser/statement.go @@ -38,42 +38,49 @@ func (s Statements) DMLTables(rawSQL string) []string { var tables = make(map[string]bool) var result []string for _, statement := range s { + // Only consider exec statements for DML table extraction. + if !statement.IsExec { + continue + } SQL := rawSQL[statement.Start:statement.End] - usesService := strings.Contains(SQL, "$sql.") - lowerCasedDML := strings.ToLower(SQL) - quoted := "" - - if index := strings.Index(SQL, `"`); index != -1 { - quoted = SQL[index+1:] - if index = strings.Index(quoted, `"`); index != -1 { - quoted = quoted[:index] + // Handle service-based exec ($sql.Insert/$sql.Update) only when explicitly detected as service. + if statement.Kind == shared.ExecKindService { + quoted := "" + if index := strings.Index(SQL, `"`); index != -1 { + quoted = SQL[index+1:] + if index = strings.Index(quoted, `"`); index != -1 { + quoted = quoted[:index] + } } - } - if usesService && quoted != "" { - statement.Table = quoted - if _, ok := tables[statement.Table]; ok { + if quoted != "" { + statement.Table = quoted + if _, ok := tables[statement.Table]; ok { + continue + } + result = append(result, statement.Table) + tables[statement.Table] = true continue } - result = append(result, statement.Table) - tables[statement.Table] = true - continue } + + lowerCasedDML := strings.ToLower(SQL) + if strings.Contains(lowerCasedDML, "insert") { - if stmt, _ := sqlparser.ParseInsert(SQL); stmt != nil { + if stmt, _ := sqlparser.ParseInsert(SQL); stmt != nil && stmt.Target.X != nil { if table := sqlparser.Stringify(stmt.Target.X); table != "" { statement.Table = table } } } else if strings.Contains(lowerCasedDML, "update") { - if stmt, _ := sqlparser.ParseUpdate(SQL); stmt != nil { + if stmt, _ := sqlparser.ParseUpdate(SQL); stmt != nil && stmt.Target.X != nil { if table := sqlparser.Stringify(stmt.Target.X); table != "" { statement.Table = table } } } else if strings.Contains(lowerCasedDML, "delete") { - if stmt, _ := sqlparser.ParseDelete(SQL); stmt != nil { + if stmt, _ := sqlparser.ParseDelete(SQL); stmt != nil && stmt.Target.X != nil { if table := sqlparser.Stringify(stmt.Target.X); table != "" { statement.Table = table } diff --git a/internal/translator/resource.go b/internal/translator/resource.go index eb92605b8..db7e97121 100644 --- a/internal/translator/resource.go +++ b/internal/translator/resource.go @@ -3,6 +3,10 @@ package translator import ( "context" "fmt" + "path" + "reflect" + "strings" + "github.com/viant/afs" "github.com/viant/afs/url" "github.com/viant/datly/cmd/options" @@ -22,9 +26,6 @@ import ( "github.com/viant/toolbox" "github.com/viant/xreflect" "golang.org/x/mod/modfile" - "path" - "reflect" - "strings" ) type ( @@ -352,6 +353,15 @@ func (r *Resource) buildParameterViews() { if parameter.Cache != "" { viewlet.View.Cache = &view.Cache{Reference: shared.Reference{Ref: parameter.Cache}} } + if parameter.Limit != nil { + if viewlet.View.Selector == nil { + viewlet.View.Selector = &view.Config{ + Constraints: &view.Constraints{Limit: true}, + } + } + viewlet.View.Selector.Limit = *parameter.Limit + viewlet.View.Selector.NoLimit = viewlet.View.Selector.Limit == 0 + } if viewlet.Connector == "" { viewlet.Connector = r.rootConnector } @@ -464,9 +474,11 @@ func (r *Resource) expandSQL(viewlet *Viewlet) (*sqlx.SQL, error) { func (r *Resource) ensureViewParametersSchema(ctx context.Context, setType func(ctx context.Context, setType *Viewlet) error) error { viewParameters := r.State.FilterByKind(state.KindView) for _, viewParameter := range viewParameters { - if viewParameter.Schema != nil && viewParameter.Schema.Type() != nil { - continue - } + //WE DO NOT NEEDED IT + //if viewParameter.Schema != nil && viewParameter.Schema.Type() != nil { + // fmt.Printf("skipping view %v %v\n", viewParameter.Name, viewParameter.Schema) + // //continue + //} if viewParameter.In.Name == "" { //default root schema continue } diff --git a/internal/translator/rule.go b/internal/translator/rule.go index 98ba973a2..ec42c5388 100644 --- a/internal/translator/rule.go +++ b/internal/translator/rule.go @@ -67,6 +67,7 @@ type ( IsGeneratation bool XMLUnmarshalType string `json:",omitempty"` JSONUnmarshalType string `json:",omitempty"` + JSONMarshalType string `json:",omitempty"` OutputParameter *inference.Parameter } @@ -132,6 +133,7 @@ func (r *Rule) DSQLSetting() interface{} { DocURLs []string `json:",omitempty"` Internal bool `json:",omitempty"` JSONUnmarshalType string `json:",omitempty"` + JSONMarshalType string `json:",omitempty"` Connector string `json:",omitempty"` contract.ModelContextProtocol contract.Meta @@ -148,6 +150,7 @@ func (r *Rule) DSQLSetting() interface{} { DocURLs: r.DocURLs, Internal: r.Internal, JSONUnmarshalType: r.JSONUnmarshalType, + JSONMarshalType: r.JSONMarshalType, Connector: r.Connector, ModelContextProtocol: r.ModelContextProtocol, Meta: r.Meta, @@ -321,7 +324,9 @@ func (r *Rule) applyDefaults() { if r.XMLUnmarshalType != "" { r.Route.Content.Marshaller.XML.TypeName = r.XMLUnmarshalType } - if r.JSONUnmarshalType != "" { + if r.JSONMarshalType != "" { + r.Route.Content.Marshaller.JSON.TypeName = r.JSONMarshalType + } else if r.JSONUnmarshalType != "" { r.Route.Content.Marshaller.JSON.TypeName = r.JSONUnmarshalType } } diff --git a/internal/translator/service.go b/internal/translator/service.go index 66c9ccabb..f446521a3 100644 --- a/internal/translator/service.go +++ b/internal/translator/service.go @@ -4,6 +4,12 @@ import ( "context" "database/sql" "fmt" + "net/http" + spath "path" + "reflect" + "strings" + "time" + "github.com/viant/afs" "github.com/viant/afs/file" "github.com/viant/afs/url" @@ -14,7 +20,7 @@ import ( "github.com/viant/datly/internal/plugin" "github.com/viant/datly/internal/setter" "github.com/viant/datly/internal/translator/parser" - signature "github.com/viant/datly/repository/contract/signature" + "github.com/viant/datly/repository/contract/signature" "github.com/viant/datly/repository/path" "github.com/viant/datly/service" "github.com/viant/datly/shared" @@ -27,11 +33,6 @@ import ( "github.com/viant/xreflect" "golang.org/x/mod/modfile" "gopkg.in/yaml.v3" - "net/http" - spath "path" - "reflect" - "strings" - "time" ) type Service struct { @@ -328,6 +329,15 @@ func (s *Service) persistRouterRule(ctx context.Context, resource *Resource, ser } route.Component.Meta = resource.Rule.Meta + if route.Component.Meta.DescriptionURI != "" { + URL := url.Join(baseRuleURL, route.Component.Meta.DescriptionURI) + description, err := s.fs.DownloadWithURL(ctx, URL) + if err != nil { + return fmt.Errorf("failed to download meta description: %v %w", URL, err) + } + route.Component.Meta.Description = string(description) + } + route.ModelContextProtocol = resource.Rule.ModelContextProtocol if route.Handler != nil { if route.Component.Output.Type.Schema == nil { @@ -361,7 +371,10 @@ func (s *Service) persistRouterRule(ctx context.Context, resource *Resource, ser if resource.Rule.XMLUnmarshalType != "" { route.Content.Marshaller.XML.TypeName = resource.Rule.XMLUnmarshalType } - if resource.Rule.JSONUnmarshalType != "" { + // JSON marshaller/unmarshaller customization: prefer MarshalType if provided, fallback to UnmarshalType. + if resource.Rule.JSONMarshalType != "" { + route.Content.Marshaller.JSON.TypeName = resource.Rule.JSONMarshalType + } else if resource.Rule.JSONUnmarshalType != "" { route.Content.Marshaller.JSON.TypeName = resource.Rule.JSONUnmarshalType } route.Component.Output.DataFormat = resource.Rule.DataFormat @@ -449,6 +462,9 @@ func (s *Service) adjustView(viewlet *Viewlet, resource *Resource, mode view.Mod } if viewlet.TypeDefinition != nil { if viewlet.TypeDefinition.Cardinality == state.Many { + if viewlet.View.View.Schema == nil { + viewlet.View.View.Schema = &state.Schema{} + } viewlet.View.View.Schema.Cardinality = viewlet.TypeDefinition.Cardinality } viewlet.TypeDefinition.Cardinality = "" @@ -473,7 +489,12 @@ func (s *Service) adjustView(viewlet *Viewlet, resource *Resource, mode view.Mod if len(resource.Declarations.QuerySelectors) > 0 { for key, state := range resource.Declarations.QuerySelectors { - return fmt.Errorf("unknown query selector view %v, %v", key, state[0].Name) + switch strings.ToLower(state[0].Name) { + case "limit", "page", "offset", "fields", "orderby", "criteria": + default: + return fmt.Errorf("unknown query selector view %v, %v", key, state[0].In.Name) + + } } } @@ -561,7 +582,8 @@ func (s *Service) buildQueryViewletType(ctx context.Context, viewlet *Viewlet) e func (s *Service) buildViewletType(ctx context.Context, db *sql.DB, viewlet *Viewlet) (err error) { shared.EnsureArgs(viewlet.Expanded.Query, &viewlet.Expanded.Args) - if viewlet.Spec, err = inference.NewSpec(ctx, db, &s.Repository.Messages, viewlet.Table.Name, viewlet.ColumnConfig, viewlet.Expanded.Query, viewlet.Expanded.Args...); err != nil { + viewlet.Spec, err = inference.NewSpec(ctx, db, &s.Repository.Messages, viewlet.Table.Name, viewlet.ColumnConfig, viewlet.Expanded.Query, viewlet.Expanded.Args...) + if err != nil { return fmt.Errorf("failed to create spec for %v, %w", viewlet.Name, err) } diff --git a/internal/translator/view.go b/internal/translator/view.go index ab94c500e..fc0096ca8 100644 --- a/internal/translator/view.go +++ b/internal/translator/view.go @@ -2,15 +2,17 @@ package translator import ( "fmt" + "github.com/viant/datly/internal/asset" "github.com/viant/datly/internal/inference" "github.com/viant/datly/internal/setter" "github.com/viant/datly/internal/translator/parser" + "path" + "github.com/viant/datly/view" "github.com/viant/datly/view/state" "github.com/viant/tagly/format/text" - "path" ) type ( @@ -212,7 +214,8 @@ func (v *View) buildSelector(namespace *Viewlet, rule *Rule) { selector.PageParameter = ¶meter.Parameter selector.Constraints.Page = &enabled } - delete(namespace.Resource.Declarations.QuerySelectors, namespace.Name) + + //delete(namespace.Resource.Declarations.QuerySelectors, namespace.Name) } } diff --git a/internal/translator/viewlets.go b/internal/translator/viewlets.go index eb1d24b7b..72707387c 100644 --- a/internal/translator/viewlets.go +++ b/internal/translator/viewlets.go @@ -138,6 +138,9 @@ func (n *Viewlets) addRelations(query *query.Select) error { parentNs := inference.ParentAlias(join) parentViewlet := n.Lookup(parentNs) + if parentViewlet == nil { + return fmt.Errorf("parent viewlet %v doesn't exist", parentNs) + } relation.Spec.Parent = parentViewlet.Spec cardinality := state.Many if inference.IsToOne(join) || relation.OutputSettings.IsToOne() { diff --git a/mcp/server.go b/mcp/server.go index 2c9cd306e..b3e1c3e45 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -3,6 +3,7 @@ package mcp import ( "context" "fmt" + "github.com/viant/afs" "github.com/viant/afs/http" "github.com/viant/afs/url" @@ -15,18 +16,20 @@ import ( "github.com/viant/mcp/client/auth/transport" authserver "github.com/viant/mcp/server/auth" - serverproto "github.com/viant/mcp-protocol/server" - "github.com/viant/scy/auth/flow" "os" "path" + serverproto "github.com/viant/mcp-protocol/server" + "github.com/viant/scy/auth/flow" + + "reflect" + "strconv" + "strings" + "github.com/viant/mcp/server" "github.com/viant/scy" "github.com/viant/scy/cred" "golang.org/x/oauth2" - "reflect" - "strconv" - "strings" ) type Server struct { @@ -40,7 +43,7 @@ func (s *Server) init() error { var newImplementer = extension.New(s.registry) var options = []server.Option{ server.WithNewHandler(newImplementer), - server.WithImplementation(schema.Implementation{"Datly", "0.1"}), + server.WithImplementation(schema.Implementation{Name: "Datly", Version: "0.1"}), } issuerURL := s.config.IssuerURL var oauth2Config *oauth2.Config @@ -54,6 +57,7 @@ func (s *Server) init() error { } if issuerURL == "" && oauth2Config != nil { issuerURL, _ = url.Base(oauth2Config.Endpoint.AuthURL, http.SecureScheme) + s.config.IssuerURL = issuerURL } } authPolicy := &authorization.Policy{ diff --git a/repository/component.go b/repository/component.go index 31faed6cf..ec106e47a 100644 --- a/repository/component.go +++ b/repository/component.go @@ -4,6 +4,10 @@ import ( "context" "embed" "fmt" + "net/http" + "reflect" + "strings" + "github.com/francoispqt/gojay" "github.com/viant/afs" "github.com/viant/datly/gateway/router/marshal" @@ -11,7 +15,7 @@ import ( "github.com/viant/datly/gateway/router/marshal/json" "github.com/viant/datly/internal/setter" "github.com/viant/datly/repository/async" - "github.com/viant/datly/repository/content" + content "github.com/viant/datly/repository/content" "github.com/viant/datly/repository/contract" "github.com/viant/datly/repository/handler" "github.com/viant/datly/repository/version" @@ -29,9 +33,6 @@ import ( xhandler "github.com/viant/xdatly/handler" hstate "github.com/viant/xdatly/handler/state" "github.com/viant/xreflect" - "net/http" - "reflect" - "strings" ) // Component represents abstract API view/handler based component @@ -169,6 +170,109 @@ func (c *Component) initView(ctx context.Context, resource *view.Resource) error if err := c.View.Init(ctx, resource); err != nil { return err } + // For read components (GET), expose and enable offset/limit/fields/page/orderBy for each namespaced view. + if strings.EqualFold(c.Path.Method, http.MethodGet) { + // Helper to enable limit/offset for a view with namespace prefix (if any) + ensureSelectors := func(v *view.View, nsPrefix string) { + if v == nil { + return + } + if v.Selector == nil { + v.Selector = &view.Config{} + } + if v.Selector.Constraints == nil { + v.Selector.Constraints = &view.Constraints{} + } + // Enable constraints + v.Selector.Constraints.Limit = true + v.Selector.Constraints.Offset = true + v.Selector.Constraints.Projection = true + v.Selector.Constraints.OrderBy = true + + // Limit param + if v.Selector.LimitParameter == nil { + p := *view.QueryStateParameters.LimitParameter + p.Description = view.Description(view.LimitQuery, v.Name) + if nsPrefix != "" { + p.In = state.NewQueryLocation(nsPrefix + view.LimitQuery) + } + v.Selector.LimitParameter = &p + } else if v.Selector.LimitParameter.Description == "" { + v.Selector.LimitParameter.Description = view.Description(view.LimitQuery, v.Name) + } + + // Offset param + if v.Selector.OffsetParameter == nil { + p := *view.QueryStateParameters.OffsetParameter + p.Description = view.Description(view.OffsetQuery, v.Name) + if nsPrefix != "" { + p.In = state.NewQueryLocation(nsPrefix + view.OffsetQuery) + } + v.Selector.OffsetParameter = &p + } else if v.Selector.OffsetParameter.Description == "" { + v.Selector.OffsetParameter.Description = view.Description(view.OffsetQuery, v.Name) + } + + // Fields param (controls which fields are included) + if v.Selector.FieldsParameter == nil { + p := *view.QueryStateParameters.FieldsParameter + p.Description = view.Description(view.FieldsQuery, v.Name) + if nsPrefix != "" { + p.In = state.NewQueryLocation(nsPrefix + view.FieldsQuery) + } + v.Selector.FieldsParameter = &p + } else if v.Selector.FieldsParameter.Description == "" { + v.Selector.FieldsParameter.Description = view.Description(view.FieldsQuery, v.Name) + } + + // Page param (paging interface on top of limit/offset) + if v.Selector.PageParameter == nil { + p := *view.QueryStateParameters.PageParameter + p.Description = view.Description(view.PageQuery, v.Name) + if nsPrefix != "" { + p.In = state.NewQueryLocation(nsPrefix + view.PageQuery) + } + v.Selector.PageParameter = &p + } else if v.Selector.PageParameter.Description == "" { + v.Selector.PageParameter.Description = view.Description(view.PageQuery, v.Name) + } + + // OrderBy param + if v.Selector.OrderByParameter == nil { + p := *view.QueryStateParameters.OrderByParameter + p.Description = view.Description(view.OrderByQuery, v.Name) + if nsPrefix != "" { + p.In = state.NewQueryLocation(nsPrefix + view.OrderByQuery) + } + v.Selector.OrderByParameter = &p + } else if v.Selector.OrderByParameter.Description == "" { + v.Selector.OrderByParameter.Description = view.Description(view.OrderByQuery, v.Name) + } + } + + // Root view + nsPrefix := "" + if c.View.Selector != nil && c.View.Selector.Namespace != "" { + nsPrefix = c.View.Selector.Namespace + } + ensureSelectors(c.View, nsPrefix) + + // All related views via NamespacedView + if c.NamespacedView != nil { + for _, nsView := range c.NamespacedView.Views { + v := nsView.View + // Determine ns prefix from NamespacedView (prefer non-empty namespace if present) + pfx := "" + for _, ns := range nsView.Namespaces { + if ns != "" { + pfx = ns + break + } + } + ensureSelectors(v, pfx) + } + } + } holder := "" if c.Contract.Output.Type.Parameters != nil { if rootHolder := c.Contract.Output.Type.Parameters.LookupByLocation(state.KindOutput, "view"); rootHolder != nil { @@ -260,27 +364,143 @@ func (c *Component) IOConfig() *config.IOConfig { } func (c *Component) UnmarshalFunc(request *http.Request) shared.Unmarshal { - contentType := request.Header.Get(content.HeaderContentType) - setter.SetStringIfEmpty(&contentType, request.Header.Get(strings.ToLower(content.HeaderContentType))) + // Delegate to options-based variant for symmetry and centralization. + return c.UnmarshalFor(WithUnmarshalRequest(request)) +} + +// UnmarshalOption configures unmarshal behavior for Component.UnmarshalFor. +type UnmarshalOption func(*unmarshalOptions) + +type unmarshalOptions struct { + request *http.Request + contentType string + interceptors json.UnmarshalerInterceptors +} + +// WithUnmarshalRequest supplies an http request for content-type detection and transforms. +func WithUnmarshalRequest(r *http.Request) UnmarshalOption { + return func(o *unmarshalOptions) { o.request = r } +} + +// WithContentType overrides the detected content type. +func WithContentType(ct string) UnmarshalOption { + return func(o *unmarshalOptions) { o.contentType = ct } +} + +// WithUnmarshalInterceptors adds/overrides JSON path interceptors. +func WithUnmarshalInterceptors(m json.UnmarshalerInterceptors) UnmarshalOption { + return func(o *unmarshalOptions) { + if o.interceptors == nil { + o.interceptors = json.UnmarshalerInterceptors{} + } + for k, v := range m { + o.interceptors[k] = v + } + } +} + +// UnmarshalFor returns a request-scoped unmarshal function applying content-type detection and transforms. +func (c *Component) UnmarshalFor(opts ...UnmarshalOption) shared.Unmarshal { + options := &unmarshalOptions{} + for _, opt := range opts { + if opt != nil { + opt(options) + } + } + + // Resolve content type if request present + contentType := options.contentType + if contentType == "" && options.request != nil { + contentType = options.request.Header.Get(content.HeaderContentType) + setter.SetStringIfEmpty(&contentType, options.request.Header.Get(strings.ToLower(content.HeaderContentType))) + } + switch contentType { case content.XMLContentType: return c.Content.Marshaller.XML.Unmarshal case content.CSVContentType: return c.Content.CSV.Unmarshal - default: - switch c.Output.DataFormat { - case content.XMLFormat: - return c.Content.Marshaller.XML.Unmarshal + } + // Fallback to data format preference when no content type or not matched + if c.Output.DataFormat == content.XMLFormat { + return c.Content.Marshaller.XML.Unmarshal + } + + // Build JSON path interceptors from component transforms and any user-provided ones + interceptors := options.interceptors + if interceptors == nil { + interceptors = json.UnmarshalerInterceptors{} + } + if options.request != nil { + for _, transform := range c.UnmarshallerInterceptors() { + interceptors[transform.Path] = c.transformFn(options.request, transform) + } + } + + req := options.request // capture for closure + return func(data []byte, dest interface{}) error { + if len(interceptors) > 0 || req != nil { + return c.Content.Marshaller.JSON.JsonMarshaller.Unmarshal(data, dest, interceptors, req) + } + return c.Content.Marshaller.JSON.JsonMarshaller.Unmarshal(data, dest) + } +} + +// MarshalOption configures marshal behavior for Component.MarshalFunc. +type MarshalOption func(*marshalOptions) + +type marshalOptions struct { + request *http.Request + format string + field string + filters []*json.FilterEntry +} + +// WithRequest supplies an http request for deriving format and state-based exclusions. +func WithRequest(r *http.Request) MarshalOption { return func(o *marshalOptions) { o.request = r } } + +// WithFormat overrides the output format (e.g. content.JSONFormat, content.CSVFormat, etc.). +func WithFormat(format string) MarshalOption { return func(o *marshalOptions) { o.format = format } } + +// WithField overrides the field used by tabular JSON embedding. +func WithField(field string) MarshalOption { return func(o *marshalOptions) { o.field = field } } + +// WithFilters sets explicit JSON field filters (exclusion-based projection). +func WithFilters(filters []*json.FilterEntry) MarshalOption { + return func(o *marshalOptions) { o.filters = filters } +} + +// MarshalFunc returns a request-scoped marshaller closure applying options like format and exclusions. +// If no format is specified, it defaults to JSON for non-reader services and derives from request for readers. +func (c *Component) MarshalFunc(opts ...MarshalOption) shared.Marshal { + options := &marshalOptions{} + for _, opt := range opts { + if opt != nil { + opt(options) } } - jsonPathInterceptor := json.UnmarshalerInterceptors{} - unmarshallerInterceptors := c.UnmarshallerInterceptors() - for i := range unmarshallerInterceptors { - transform := unmarshallerInterceptors[i] - jsonPathInterceptor[transform.Path] = c.transformFn(request, transform) + + // Resolve format + format := options.format + if format == "" { + if options.request != nil && c.Service == service.TypeReader { + format = c.Output.Format(options.request.URL.Query()) + } else { + format = content.JSONFormat + } + } + + // Resolve field (used for tabular JSON embedding) + field := options.field + if field == "" { + field = c.Output.Field() } - return func(bytes []byte, i interface{}) error { - return c.Content.Marshaller.JSON.JsonMarshaller.Unmarshal(bytes, i, jsonPathInterceptor, request) + + // Resolve filters (explicit only) + filters := options.filters + + return func(src interface{}) ([]byte, error) { + return c.Content.Marshal(format, field, src, filters) } } @@ -424,6 +644,9 @@ func WithContract(inputType, outputType reflect.Type, embedFs *embed.FS, viewOpt aCache := &view.Cache{Reference: shared.Reference{Ref: aView.Cache}} viewOptions = append(viewOptions, view.WithCache(aCache)) } + if aView.Limit != nil { + viewOptions = append(viewOptions, view.WithLimit(aView.Limit)) + } if aTag.View.PublishParent { viewOptions = append(viewOptions, view.WithViewPublishParent(aTag.View.PublishParent)) @@ -441,6 +664,10 @@ func WithContract(inputType, outputType reflect.Type, embedFs *embed.FS, viewOpt if aTag.View.Batch != 0 { viewOptions = append(viewOptions, view.WithBatchSize(aTag.View.Batch)) } + if aTag.View.Limit != nil { + viewOptions = append(viewOptions, view.WithLimit(aTag.View.Limit)) + } + if aTag.View.RelationalConcurrency != 0 { viewOptions = append(viewOptions, view.WithRelationalConcurrency(aTag.View.RelationalConcurrency)) } diff --git a/repository/components.go b/repository/components.go index c803bcd3f..536ad3292 100644 --- a/repository/components.go +++ b/repository/components.go @@ -236,6 +236,9 @@ func (c *Components) updateIOTypeDependencies(ctx context.Context, ioType *state aView = baseView } } + if aView.Schema == nil { + aView.Schema = parameterViewSchema(parameter) + } aView.Schema.SetType(parameter.Schema.Type()) } } diff --git a/repository/contract/dispatcher.go b/repository/contract/dispatcher.go index 22afc3d26..6da3f9f72 100644 --- a/repository/contract/dispatcher.go +++ b/repository/contract/dispatcher.go @@ -2,6 +2,7 @@ package contract import ( "context" + "github.com/viant/xdatly/handler/logger" hstate "github.com/viant/xdatly/handler/state" "net/http" "net/url" @@ -16,6 +17,7 @@ type ( Header http.Header Form *hstate.Form Request *http.Request + Logger logger.Logger } //Option represents a dispatcher option Option func(o *Options) @@ -77,3 +79,10 @@ func WithRequest(request *http.Request) Option { o.Request = request } } + +// WithLogger adds path parameters +func WithLogger(loger logger.Logger) Option { + return func(o *Options) { + o.Logger = loger + } +} diff --git a/repository/contract/meta.go b/repository/contract/meta.go index 878128b5e..8b0cfa71a 100644 --- a/repository/contract/meta.go +++ b/repository/contract/meta.go @@ -7,8 +7,9 @@ import ( // MCP Model Configuration Protocol path integration type Meta struct { - Name string `json:",omitempty" yaml:"Name"` // name of the MCP - Description string `json:",omitempty" yaml:"Description"` // optional description for documentation purposes + Name string `json:",omitempty" yaml:"Name"` // name of the MCP + Description string `json:",omitempty" yaml:"Description"` // optional description for documentation purposes + DescriptionURI string `json:",omitempty" yaml:"DescriptionURI"` } type ModelContextProtocol struct { diff --git a/repository/locator/async/locator.go b/repository/locator/async/locator.go index bc8141cde..7d047fe42 100644 --- a/repository/locator/async/locator.go +++ b/repository/locator/async/locator.go @@ -9,13 +9,14 @@ import ( "github.com/viant/xdatly/handler/async" "github.com/viant/xdatly/handler/exec" "github.com/viant/xdatly/handler/response" + "reflect" "strings" "time" ) type Locator struct{} -func (l *Locator) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (l *Locator) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { name = strings.ToLower(name) if name == keys.JobError { diff --git a/repository/locator/component/component.go b/repository/locator/component/component.go index 1db480628..b38907df6 100644 --- a/repository/locator/component/component.go +++ b/repository/locator/component/component.go @@ -3,27 +3,31 @@ package component import ( "context" "fmt" + "net/http" + "net/url" + "reflect" + "github.com/viant/datly/repository/contract" "github.com/viant/datly/shared" "github.com/viant/datly/view/state" "github.com/viant/datly/view/state/kind" "github.com/viant/datly/view/state/kind/locator" + "github.com/viant/xdatly/handler/logger" "github.com/viant/xdatly/handler/response" hstate "github.com/viant/xdatly/handler/state" "github.com/viant/xunsafe" - "net/http" - "net/url" - "reflect" ) type componentLocator struct { - custom []interface{} - dispatch contract.Dispatcher - constants map[string]interface{} - path map[string]string - form *hstate.Form - query url.Values - header http.Header + custom []interface{} + dispatch contract.Dispatcher + constants map[string]interface{} + path map[string]string + form *hstate.Form + query url.Values + header http.Header + logger logger.Logger + getRequest func() (*http.Request, error) } @@ -31,7 +35,7 @@ func (l *componentLocator) Names() []string { return nil } -func (l *componentLocator) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (l *componentLocator) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { method, URI := shared.ExtractPath(name) request, err := l.getRequest() if err != nil { @@ -43,6 +47,7 @@ func (l *componentLocator) Value(ctx context.Context, name string) (interface{}, contract.WithPath(l.path), contract.WithQuery(l.query), contract.WithForm(form), + contract.WithLogger(l.logger), contract.WithHeader(l.header), ) err = updateErrWithResponseStatus(err, value) @@ -102,6 +107,7 @@ func newComponentLocator(opts ...locator.Option) (kind.Locator, error) { dispatch: options.Dispatcher, constants: options.Constants, getRequest: options.GetRequest, + logger: options.Logger, form: options.Form, query: options.Query, header: options.Header, diff --git a/repository/locator/component/dispatcher/disptacher.go b/repository/locator/component/dispatcher/disptacher.go index 0ea2fbd3b..0c13a136f 100644 --- a/repository/locator/component/dispatcher/disptacher.go +++ b/repository/locator/component/dispatcher/disptacher.go @@ -47,6 +47,7 @@ func (d *Dispatcher) Dispatch(ctx context.Context, path *contract.Path, opts ... aSession := session.New(aComponent.View, session.WithLocatorOptions(options...), session.WithAuth(d.auth), session.WithRegistry(d.registry), + session.WithLogger(cOptions.Logger), session.WithComponent(aComponent), session.WithOperate(d.service.Operate)) ctx = aSession.Context(ctx, true) diff --git a/repository/locator/meta/locator.go b/repository/locator/meta/locator.go index 487a58be8..ba1b0c1af 100644 --- a/repository/locator/meta/locator.go +++ b/repository/locator/meta/locator.go @@ -7,13 +7,14 @@ import ( "github.com/viant/datly/view/state" "github.com/viant/datly/view/state/kind" "github.com/viant/datly/view/state/kind/locator" + "reflect" "strings" ) type metaLocator struct { } -func (l *metaLocator) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (l *metaLocator) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { value := ctx.Value(view.ContextKey) if value == nil { return nil, false, nil diff --git a/repository/locator/output/output.go b/repository/locator/output/output.go index c9368971a..ec6b3ed5f 100644 --- a/repository/locator/output/output.go +++ b/repository/locator/output/output.go @@ -3,6 +3,7 @@ package output import ( "context" "encoding/json" + "reflect" "strings" "github.com/viant/datly/repository/locator/output/keys" @@ -25,7 +26,7 @@ func (l *Locator) Names() []string { return nil } -func (l *Locator) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (l *Locator) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { aName := strings.ToLower(name) switch aName { case keys.ViewData: diff --git a/repository/logging/logging.go b/repository/logging/logging.go index 2383e2f11..b618199ac 100644 --- a/repository/logging/logging.go +++ b/repository/logging/logging.go @@ -1,48 +1,93 @@ package logging import ( + "encoding/json" "fmt" - "github.com/goccy/go-json" - "github.com/viant/xdatly/handler/exec" + "reflect" + "runtime/debug" "strconv" - "time" + + "github.com/viant/xdatly/handler/exec" ) func Log(config *Config, execContext *exec.Context) { - execContext.ElapsedMs = int(time.Since(execContext.StartTime).Milliseconds()) + snap := execContext.SnapshotForLogging() includeSQL := config.ShallIncludeSQL() if !includeSQL { - execContext.Metrics = execContext.Metrics.HideMetrics() + snap.Metrics = snap.Metrics.HideMetrics() } if config.IsAuditEnabled() { - data, _ := json.Marshal(execContext) - fmt.Println("[AUDIT] " + string(data)) + data := safeMarshal("EXECCONTEXT", snap) + fmt.Println("[AUDIT]", string(data)) } if config.IsTracingEnabled() { - trace := execContext.Trace + trace := snap.Trace rootSpan := trace.Spans[0] - spans := execContext.Metrics.ToSpans(&rootSpan.SpanID) - if execContext.Auth != nil { - if execContext.Auth.UserID != 0 { - rootSpan.Attributes["jwt.uid"] = strconv.Itoa(execContext.Auth.UserID) + spans := snap.Metrics.ToSpans(&rootSpan.SpanID) + if snap.Auth != nil { + if snap.Auth.UserID != 0 { + rootSpan.Attributes["jwt.uid"] = strconv.Itoa(snap.Auth.UserID) } - if execContext.Auth.Username != "" { - rootSpan.Attributes["jwt.username"] = execContext.Auth.Username + if snap.Auth.Username != "" { + rootSpan.Attributes["jwt.username"] = snap.Auth.Username } - if execContext.Auth.Email != "" { - rootSpan.Attributes["jwt.email"] = execContext.Auth.Email + if snap.Auth.Email != "" { + rootSpan.Attributes["jwt.email"] = snap.Auth.Email } - if execContext.Auth.Scope != "" { - rootSpan.Attributes["jwt.scope"] = execContext.Auth.Scope + if snap.Auth.Scope != "" { + rootSpan.Attributes["jwt.scope"] = snap.Auth.Scope } } trace.Append(spans...) - if execContext.Error != "" { - trace.Spans[0].SetStatus(fmt.Errorf(execContext.Error)) + if snap.Error != "" { + trace.Spans[0].SetStatus(fmt.Errorf(snap.Error)) } else { - trace.Spans[0].SetStatusFromHTTPCode(execContext.StatusCode) + trace.Spans[0].SetStatusFromHTTPCode(snap.StatusCode) + } + traceData := safeMarshal("TRACE", trace) + fmt.Println("[TRACE]", string(traceData)) + } +} + +func safeMarshal(label string, v any) []byte { + defer func() { + if r := recover(); r != nil { + fmt.Printf("[LOG-MARSHAL-PANIC] label=%s type=%T panic=%v\nSTACK:\n%s\n", label, v, r, debug.Stack()) + if execCtx, ok := v.(*exec.Context); ok { + findBadField(execCtx) + } } - traceData, _ := json.Marshal(trace) - fmt.Println("[TRACE] " + string(traceData)) + }() + data, err := json.Marshal(v) + if err != nil { + fmt.Printf("[LOG-MARSHAL-ERROR] label=%s type=%T err=%v\n", label, v, err) + return nil + } + return data +} + +func findBadField(execCtx *exec.Context) { + val := reflect.ValueOf(execCtx).Elem() + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + fieldName := fieldType.Name + + // Skip unexported fields + if !field.CanInterface() { + continue + } + + func() { + defer func() { + if r := recover(); r != nil { + fmt.Printf("[BAD-FIELD-PANIC] %s (%s): %v\n", fieldName, field.Type(), r) + } + }() + if _, err := json.Marshal(field.Interface()); err != nil { + fmt.Printf("[BAD-FIELD-ERROR] %s (%s): %v\n", fieldName, field.Type(), err) + } + }() } } diff --git a/repository/logging/logging_test.go b/repository/logging/logging_test.go new file mode 100644 index 000000000..a9beb4293 --- /dev/null +++ b/repository/logging/logging_test.go @@ -0,0 +1,194 @@ +package logging + +import ( + "bytes" + "encoding/json" + "io" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/viant/xdatly/handler/exec" +) + +// TestSafeMarshal_Success tests successful JSON marshaling +func TestSafeMarshal_Success(t *testing.T) { + type TestStruct struct { + Name string `json:"name"` + Value int `json:"value"` + } + + testData := TestStruct{ + Name: "test", + Value: 42, + } + + result := safeMarshal("TEST", testData) + assert.NotNil(t, result, "safeMarshal should return non-nil for valid data") + + var unmarshaled TestStruct + err := json.Unmarshal(result, &unmarshaled) + assert.NoError(t, err) + assert.Equal(t, testData, unmarshaled) +} + +// TestSafeMarshal_Error tests safeMarshal with a value that causes a marshaling error +func TestSafeMarshal_Error(t *testing.T) { + // Channel cannot be marshaled to JSON + ch := make(chan int) + result := safeMarshal("TEST", ch) + assert.Nil(t, result, "safeMarshal should return nil when marshaling fails") +} + +// TestSafeMarshal_Panic tests safeMarshal with a value that causes a panic +func TestSafeMarshal_Panic(t *testing.T) { + // Function cannot be marshaled and may cause panic + fn := func() {} + result := safeMarshal("TEST", fn) + assert.Nil(t, result, "safeMarshal should return nil when marshaling panics") +} + +// TestSafeMarshal_ExecContext tests safeMarshal with exec.Context +func TestSafeMarshal_ExecContext(t *testing.T) { + execCtx := exec.NewContext("GET", "/test", nil, "") + result := safeMarshal("EXECCONTEXT", execCtx) + + // Should either succeed (return non-nil) or fail gracefully (return nil) + // The important thing is it doesn't panic + if result != nil { + assert.NotEmpty(t, result) + } +} + +// TestSafeMarshal_NilValue tests safeMarshal with nil value +func TestSafeMarshal_NilValue(t *testing.T) { + result := safeMarshal("TEST", nil) + assert.NotNil(t, result) + assert.Equal(t, []byte("null"), result) +} + +// TestFindBadField_ValidExecContext tests findBadField with a valid exec.Context +func TestFindBadField_ValidExecContext(t *testing.T) { + // Capture stdout to check output + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + execCtx := exec.NewContext("GET", "/test", nil, "") + findBadField(execCtx) + + // Close write pipe and restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + io.Copy(&buf, r) + output := buf.String() + + // With a valid exec.Context, there should be no bad field errors + assert.NotContains(t, output, "[BAD-FIELD-ERROR]", "valid exec.Context should not have bad fields") + assert.NotContains(t, output, "[BAD-FIELD-PANIC]", "valid exec.Context should not panic on field marshaling") +} + +// TestFindBadField_CompletesWithoutPanic tests that findBadField completes without panicking +func TestFindBadField_CompletesWithoutPanic(t *testing.T) { + execCtx := exec.NewContext("GET", "/test", nil, "") + + // Should complete without panicking + assert.NotPanics(t, func() { + findBadField(execCtx) + }) +} + +// TestSafeMarshal_WithLabel tests that safeMarshal uses the label parameter in error messages +func TestSafeMarshal_WithLabel(t *testing.T) { + // Capture stdout to verify label is used in error messages + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Use a value that will cause an error + ch := make(chan int) + result := safeMarshal("CUSTOM_LABEL", ch) + + // Close write pipe and restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + io.Copy(&buf, r) + output := buf.String() + + assert.Nil(t, result, "should return nil on error") + if strings.Contains(output, "[LOG-MARSHAL-ERROR]") { + assert.Contains(t, output, "CUSTOM_LABEL", "error message should include the label") + } +} + +// TestSafeMarshal_RecoversFromPanic tests that safeMarshal properly recovers from panics +func TestSafeMarshal_RecoversFromPanic(t *testing.T) { + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Create a type that will panic during JSON marshaling + type PanicType struct { + Value func() // Functions cannot be marshaled + } + + panicValue := PanicType{ + Value: func() {}, + } + + // This should not cause the test to panic + result := safeMarshal("PANIC_TEST", panicValue) + + // Close write pipe and restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + io.Copy(&buf, r) + output := buf.String() + + // Function should recover and return nil + assert.Nil(t, result, "should return nil after recovering from panic") + // Should log the panic + if strings.Contains(output, "[LOG-MARSHAL-PANIC]") { + assert.Contains(t, output, "PANIC_TEST", "panic log should include label") + } +} + +// TestSafeMarshal_ExecContextPanicCallsFindBadField tests that safeMarshal calls findBadField when exec.Context panics +func TestSafeMarshal_ExecContextPanicCallsFindBadField(t *testing.T) { + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + execCtx := exec.NewContext("GET", "/test", nil, "") + + // Try to marshal - if it panics, findBadField should be called + result := safeMarshal("EXECCONTEXT", execCtx) + + // Close write pipe and restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + io.Copy(&buf, r) + output := buf.String() + + // If marshaling panicked, findBadField should have been called + if result == nil && strings.Contains(output, "[LOG-MARSHAL-PANIC]") { + // findBadField should have been called (though output may be empty if no bad fields found) + // The important thing is that the function didn't crash + assert.True(t, true, "findBadField should be called when exec.Context panics") + } +} diff --git a/repository/resource/service.go b/repository/resource/service.go index d1104e891..19d098b54 100644 --- a/repository/resource/service.go +++ b/repository/resource/service.go @@ -3,6 +3,10 @@ package resource import ( "context" "fmt" + "strings" + "sync" + "time" + "github.com/viant/afs" "github.com/viant/afs/file" "github.com/viant/afs/storage" @@ -10,9 +14,6 @@ import ( "github.com/viant/cloudless/resource" "github.com/viant/datly/repository/version" "github.com/viant/datly/view" - "strings" - "sync" - "time" ) type ( diff --git a/router.go b/router.go new file mode 100644 index 000000000..03504a4eb --- /dev/null +++ b/router.go @@ -0,0 +1,130 @@ +package datly + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/viant/datly/repository" + "github.com/viant/datly/repository/contract" + "github.com/viant/xdatly/handler/response" + hstate "github.com/viant/xdatly/handler/state" +) + +type Handler[T any] func(ctx context.Context, service T, request *http.Request, injector hstate.Injector, extra ...OperateOption) (interface{}, error) + +type Route[T any] struct { + dao *Service + handler Handler[T] + service T + path *contract.Path + component *repository.Component +} + +func (r Route[T]) ensureComponent(ctx context.Context) (*repository.Component, error) { + if r.component == nil { + var err error + r.component, err = r.dao.repository.Registry().Lookup(ctx, r.path) + if err != nil { + return nil, err + } + } + return r.component, nil +} + +func (r Route[T]) Run(ctx context.Context, writer http.ResponseWriter, request *http.Request) error { + marshaller, contentType, _, err := r.dao.getMarshaller(request, r.component) + if err != nil { + return fmt.Errorf("failed to lookup marshaller: %w", err) + } + injector, err := r.dao.GetInjector(request, r.component) + if err != nil { + return fmt.Errorf("failed to lookup injector: %w", err) + } + selectors := []*hstate.NamedQuerySelector{} + values := request.URL.Query() + if page := values.Get("page"); page != "" { + selector := &hstate.NamedQuerySelector{Name: r.component.View.Name} + selector.Page, _ = strconv.Atoi(page) + selectors = append(selectors, selector) + } + result, err := r.handler(ctx, r.service, request, injector, WithSessionOptions(WithRequest(request), WithQuerySelectors(selectors...))) + var data []byte + if err != nil { + rErr, ok := err.(*response.Error) + if !ok { + rErr = response.NewError(http.StatusInternalServerError, err.Error()) + } + data, err = marshaller(rErr) + } else { + data, err = marshaller(result) + } + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return nil + } + statusCode := http.StatusOK + statusCoder, ok := result.(response.StatusCoder) + if ok { + statusCode = statusCoder.StatusCode() + } + + writer.Header().Set("Content-Type", contentType) + writer.WriteHeader(statusCode) + _, err = writer.Write(data) + return err +} + +func newRoute[T any](dao *Service, path *contract.Path, component *repository.Component, service T, handler Handler[T]) *Route[T] { + return &Route[T]{path: path, handler: handler, dao: dao, component: component, service: service} +} + +type Router[T any] struct { + registry map[string]*Route[T] + dao *Service + service T +} + +type routeNotFound struct { + error +} + +// IsRouteNotFound checks if error is route not found +func IsRouteNotFound(err error) bool { + _, ok := err.(*routeNotFound) + return ok +} + +func (r *Router[T]) Run(writer http.ResponseWriter, request *http.Request) error { + aPath := contract.NewPath(request.Method, request.URL.Path) + component, err := r.dao.repository.Registry().Lookup(request.Context(), aPath) + if err != nil { + fmt.Println(err) + return &routeNotFound{err} + } + route, ok := r.registry[component.Path.Key()] + if !ok { + return &routeNotFound{errors.New("route not found")} + } + return route.Run(request.Context(), writer, request) +} + +func (r *Router[T]) Register(ctx context.Context, path *contract.Path, handler Handler[T]) error { + component, err := r.dao.repository.Registry().Lookup(ctx, path) + if err != nil { + return fmt.Errorf("failed to lookup component: %w for path: %+v", err, path) + } + route := newRoute[T](r.dao, path, component, r.service, handler) + r.registry[path.Key()] = route + return nil +} + +func NewRouter[T any](dao *Service, service T) *Router[T] { + return &Router[T]{registry: make(map[string]*Route[T]), dao: dao, service: service} +} + +type BodyEnvelope[T any] struct { + Body T `parameter:",kind=body"` +} diff --git a/service.go b/service.go index 68f537d55..32a2cdcc4 100644 --- a/service.go +++ b/service.go @@ -4,33 +4,39 @@ import ( "context" _ "embed" "fmt" + "github.com/viant/cloudless/async/mbus" "github.com/viant/datly/gateway" "github.com/viant/datly/repository" + rcontent "github.com/viant/datly/repository/content" "github.com/viant/datly/repository/contract" "github.com/viant/datly/repository/locator/component/dispatcher" + srv "github.com/viant/datly/service" sjwt "github.com/viant/datly/service/auth/jwt" "github.com/viant/datly/service/auth/mock" "github.com/viant/datly/service/executor" "github.com/viant/datly/service/operator" "github.com/viant/datly/service/reader" "github.com/viant/datly/service/session" + "github.com/viant/datly/shared" "github.com/viant/datly/view" "github.com/viant/datly/view/extension" + "github.com/viant/datly/view/state/kind/locator" verifier2 "github.com/viant/scy/auth/jwt/verifier" hstate "github.com/viant/xdatly/handler/state" + "net/http" + nurl "net/url" + "reflect" + "strings" + "time" + "github.com/viant/datly/view/state" "github.com/viant/scy/auth/jwt" "github.com/viant/scy/auth/jwt/signer" "github.com/viant/structology" "github.com/viant/xdatly/codec" xhandler "github.com/viant/xdatly/handler" - "net/http" - nurl "net/url" - "reflect" - "strings" - "time" ) //go:embed Version @@ -50,16 +56,18 @@ type ( } sessionOptions struct { - request *http.Request - resource state.Resource - form *hstate.Form + request *http.Request + resource state.Resource + form *hstate.Form + querySelectors []*hstate.NamedQuerySelector } SessionOption func(o *sessionOptions) operateOptions struct { - path *contract.Path - component *repository.Component - session *session.Session + path *contract.Path + component *repository.Component + session *session.Session + output interface{} input interface{} sessionOptions []SessionOption @@ -151,6 +159,12 @@ func WithForm(form *hstate.Form) SessionOption { } } +func WithQuerySelectors(selectors ...*hstate.NamedQuerySelector) SessionOption { + return func(o *sessionOptions) { + o.querySelectors = selectors + } +} + func WithStateResource(resource state.Resource) SessionOption { return func(o *sessionOptions) { o.resource = resource @@ -160,8 +174,12 @@ func WithStateResource(resource state.Resource) SessionOption { func (s *Service) NewComponentSession(aComponent *repository.Component, opts ...SessionOption) *session.Session { sessionOpt := newSessionOptions(opts) options := aComponent.LocatorOptions(sessionOpt.request, sessionOpt.form, aComponent.UnmarshalFunc(sessionOpt.request)) + if sessionOpt.querySelectors != nil { + options = append(options, locator.WithQuerySelectors(sessionOpt.querySelectors)) + } aSession := session.New(aComponent.View, session.WithLocatorOptions(options...), session.WithAuth(s.repository.Auth()), + session.WithComponent(aComponent), session.WithStateResource(sessionOpt.resource), session.WithOperate(s.operator.Operate)) return aSession } @@ -268,6 +286,115 @@ func (s *Service) PopulateInput(ctx context.Context, aComponent *repository.Comp return nil } +func (s *Service) GetInjector(r *http.Request, comp *repository.Component) (hstate.Injector, error) { + if err := s.ensureComponentInitialized(comp); err != nil { + return nil, err + } + // Build component session to populate state (for exclusion filters) + sess := s.NewComponentSession(comp, WithRequest(r), WithStateResource(comp.View.Resource())) + return sess, nil +} + +// GetMarshaller prepares a request-scoped marshaller closure and resolved content type for the given component path. +// It preserves existing behavior for readers (format derived from query) and defaults to JSON otherwise. +func (s *Service) GetMarshaller(r *http.Request, methodAndPath string, extra ...repository.MarshalOption) (marshal shared.Marshal, contentType string, comp *repository.Component, err error) { + comp, err = s.Component(r.Context(), methodAndPath) + if err != nil || comp == nil { + if err == nil { + err = fmt.Errorf("component not found: %s", methodAndPath) + } + return nil, "", nil, err + } + return s.getMarshaller(r, comp, extra...) +} + +func (s *Service) getMarshaller(r *http.Request, comp *repository.Component, extra ...repository.MarshalOption) (shared.Marshal, string, *repository.Component, error) { + // Ensure component content marshallers are initialized (defensive when invoked outside router lifecycle) + if err := s.ensureComponentInitialized(comp); err != nil { + return nil, "", nil, err + } + + // Build component session to populate state (for exclusion filters) + sess := s.NewComponentSession(comp, WithRequest(r), WithStateResource(comp.View.Resource())) + // Compute JSON field filters from populated state + filters := comp.Exclusion(sess.State()) + + // Optional format override from query parameter `format` + override := strings.TrimSpace(r.URL.Query().Get("format")) + + var opts []repository.MarshalOption + opts = append(opts, repository.WithRequest(r), repository.WithFilters(filters)) + if override != "" { + opts = append(opts, repository.WithFormat(override)) + } + if len(extra) > 0 { + opts = append(opts, extra...) + } + + // Prepare marshaller closure + marshal := comp.MarshalFunc(opts...) + + // Resolve content type for headers + resolved := override + if resolved == "" && comp.Service == srv.TypeReader { + resolved = comp.Output.Format(r.URL.Query()) + } + if resolved == "" { + resolved = rcontent.JSONFormat + } + contentType := comp.Output.ContentType(resolved) + return marshal, contentType, comp, nil +} + +// GetUnmarshaller prepares a request-scoped unmarshaller for the given component path. +func (s *Service) GetUnmarshaller(r *http.Request, methodAndPath string, extra ...repository.UnmarshalOption) (unmarshal shared.Unmarshal, comp *repository.Component, err error) { + comp, err = s.Component(r.Context(), methodAndPath) + if err != nil || comp == nil { + if err == nil { + err = fmt.Errorf("component not found: %s", methodAndPath) + } + return nil, nil, err + } + return s.getUnmarshaller(r, comp, extra...) +} + +func (s *Service) getUnmarshaller(r *http.Request, comp *repository.Component, extra ...repository.UnmarshalOption) (shared.Unmarshal, *repository.Component, error) { + // Ensure component content marshallers are initialized (defensive) + if err := s.ensureComponentInitialized(comp); err != nil { + return nil, nil, err + } + var opts []repository.UnmarshalOption + opts = append(opts, repository.WithUnmarshalRequest(r)) + if len(extra) > 0 { + opts = append(opts, extra...) + } + unmarshal := comp.UnmarshalFor(opts...) + return unmarshal, comp, nil +} + +// ensureComponentInitialized defensively initializes component content marshallers when called from external contexts. +func (s *Service) ensureComponentInitialized(comp *repository.Component) error { + if comp == nil { + return fmt.Errorf("component was nil") + } + res := comp.View.GetResource() + if res == nil { + return nil + } + // If JSON marshaller already present, assume initialized. + if comp.Content.Marshaller.JSON.JsonMarshaller != nil { + return nil + } + // Initialize content marshallers as in Component.Init + if err := comp.Content.InitMarshaller(comp.IOConfig(), comp.Output.Exclude, comp.BodyType(), comp.OutputType()); err != nil { + return err + } + if err := comp.Content.Marshaller.Init(res.LookupType()); err != nil { + return err + } + return nil +} + // Read reads data from a view func (s *Service) Read(ctx context.Context, locator string, dest interface{}, option ...reader.Option) error { aView, err := s.View(ctx, wrapWithMethod(http.MethodGet, locator)) @@ -517,7 +644,7 @@ func (s *Service) HTTPHandler(ctx context.Context, options ...gateway.Option) (h return s.handler, nil } -// New creates a datly service, repository allows you to bootstrap empty or existing yaml repository +// New creates a dao dao, repository allows you to bootstrap empty or existing yaml repository func New(ctx context.Context, options ...repository.Option) (*Service, error) { options = append([]repository.Option{ repository.WithJWTSigner(mock.HmacJwtSigner()), diff --git a/service/executor/expand/data_unit.go b/service/executor/expand/data_unit.go index 66a694dad..9d2c22dad 100644 --- a/service/executor/expand/data_unit.go +++ b/service/executor/expand/data_unit.go @@ -17,23 +17,25 @@ import ( type ( DataUnit struct { - Columns codec.ColumnsSource - ParamsGroup []interface{} - Mock bool - TemplateSQL string - MetaSource Dber `velty:"-"` - Statements *Statements `velty:"-"` - + Columns codec.ColumnsSource + ParamsGroup []interface{} + Mock bool + TemplateSQL string + MetaSource Dber `velty:"-"` + Statements *Statements `velty:"-"` mu sync.Mutex `velty:"-"` placeholderCounter int `velty:"-"` sqlxValidator *validator.Service `velty:"-"` sliceIndex map[reflect.Type]*xunsafe.Slice `velty:"-"` ctx context.Context `velty:"-"` + EvalLock sync.Mutex } ExecutablesIndex map[string]*Executable ) +// + func (c *DataUnit) WithPresence() interface{} { var opt interface{} = validator.WithSetMarker() return opt @@ -43,17 +45,6 @@ func (c *DataUnit) WithLocation(loc string) interface{} { return opt } -// Reset clears binding-related state so DataUnit can be safely reused for a new evaluation -func (c *DataUnit) Reset() { - c.mu.Lock() - c.placeholderCounter = 0 - if len(c.ParamsGroup) > 0 { - c.ParamsGroup = c.ParamsGroup[:0] - } - c.TemplateSQL = "" - c.mu.Unlock() -} - func (c *DataUnit) Validate(dest interface{}, opts ...interface{}) (*validator.Validation, error) { db, err := c.MetaSource.Db() if err != nil { @@ -157,7 +148,7 @@ func (c *DataUnit) Next() (interface{}, error) { return c.ParamsGroup[index], nil } - return nil, fmt.Errorf("expected to get binding parameter, but noone was found, ParamsGroup: %v, placeholderCounter: %v", c.ParamsGroup, c.placeholderCounter) + return nil, fmt.Errorf("expected to get binding parameter, but none was found, ParamsGroup: %v, placeholderCounter: %v", c.ParamsGroup, c.placeholderCounter) } func (c *DataUnit) ensureSliceIndex() { @@ -187,6 +178,12 @@ func (c *DataUnit) addAll(args ...interface{}) { c.mu.Unlock() } +func (c *DataUnit) Shrink(offset int) { + c.mu.Lock() + c.ParamsGroup = c.ParamsGroup[:offset] + c.mu.Unlock() +} + func (c *DataUnit) IsServiceExec(SQL string) (*Executable, bool) { return c.Statements.LookupExecutable(SQL) } @@ -278,6 +275,17 @@ func (c *DataUnit) Like(columnName string, args interface{}) (string, error) { func (c *DataUnit) NotLike(columnName string, args interface{}) (string, error) { return c.like(columnName, args, false) } +func (c *DataUnit) Expression(expr string, value interface{}) (string, error) { + return c.expression(expr, value) +} + +func (c *DataUnit) expression(expr string, value interface{}) (string, error) { + if value == "" { + return "", nil + } + c.addAll(value) + return expr, nil +} func (c *DataUnit) like(columnName string, args interface{}, inclusive bool) (string, error) { expander, err := bindingsCache.Lookup(args) diff --git a/service/executor/expand/evaluator.go b/service/executor/expand/evaluator.go index d57b935b2..c733aca79 100644 --- a/service/executor/expand/evaluator.go +++ b/service/executor/expand/evaluator.go @@ -252,7 +252,7 @@ func (e *Evaluator) ensureState(ctx *Context, options ...StateOption) *State { state.Context = ctx } - state.Init(e.stateProvider(), e.predicateConfigs, options...) + state.Init(e.stateProvider(), e.predicateConfigs, e.stateType, options...) return state } diff --git a/service/executor/expand/predicate.go b/service/executor/expand/predicate.go index b67e83312..aae51c8bb 100644 --- a/service/executor/expand/predicate.go +++ b/service/executor/expand/predicate.go @@ -38,7 +38,11 @@ type ( } ) -func NewPredicate(ctx *Context, state *structology.State, config []*PredicateConfig) *Predicate { +func NewPredicate(ctx *Context, state *structology.State, config []*PredicateConfig, stateType *structology.StateType) *Predicate { + // Initialize state if not provided, but never override an existing state + if state == nil && stateType != nil { + state = stateType.NewState() + } return &Predicate{ ctx: ctx, config: config, @@ -129,6 +133,10 @@ func (p *Predicate) expand(group int, operator string) (string, error) { } ctx = vcontext.WithValue(ctx, PredicateCtx, p.ctx) ctx = vcontext.WithValue(ctx, PredicateState, p.state) + + p.ctx.DataUnit.EvalLock.Lock() + defer p.ctx.DataUnit.EvalLock.Unlock() + if p.ctx.Session != nil { aLogger := p.ctx.Session.Logger() ctx = vcontext.WithValue(ctx, logger.ContextKey, aLogger) diff --git a/service/executor/expand/state.go b/service/executor/expand/state.go index f970ff679..6399be43d 100644 --- a/service/executor/expand/state.go +++ b/service/executor/expand/state.go @@ -2,6 +2,7 @@ package expand import ( "context" + "github.com/viant/datly/service/executor/extension" "github.com/viant/datly/view/state/predicate" @@ -83,7 +84,7 @@ func WithCustomContext(customContext *Variable) StateOption { } } -func (s *State) Init(templateState *est.State, predicates []*PredicateConfig, options ...StateOption) { +func (s *State) Init(templateState *est.State, predicates []*PredicateConfig, stateType *structology.StateType, options ...StateOption) { for _, option := range options { option(s) } @@ -103,8 +104,6 @@ func (s *State) Init(templateState *est.State, predicates []*PredicateConfig, op if s.DataUnit == nil { s.DataUnit = NewDataUnit(nil) } - // Ensure bindings/cursor are reset for a fresh evaluation cycle - s.DataUnit.Reset() if s.Http == nil { s.Http = &Http{} @@ -122,7 +121,7 @@ func (s *State) Init(templateState *est.State, predicates []*PredicateConfig, op s.MessageBus = s.Session.MessageBus() } - s.Predicate = NewPredicate(s.Context, s.ParametersState, predicates) + s.Predicate = NewPredicate(s.Context, s.ParametersState, predicates, stateType) s.State = templateState } @@ -149,6 +148,6 @@ func StateWithSQL(ctx context.Context, SQL string) *State { Context: &Context{Context: ctx}, } - aState.Init(nil, nil) + aState.Init(nil, nil, nil) return aState } diff --git a/service/executor/extension/session.go b/service/executor/extension/session.go index d52f72f5d..9fbfe51fc 100644 --- a/service/executor/extension/session.go +++ b/service/executor/extension/session.go @@ -19,7 +19,7 @@ import ( type ( Session struct { sqlService SqlServiceFn - stater state.Stater + injector state.Injector validator *validator.Service differ *differ.Service mbus *xmbus.Service @@ -92,7 +92,7 @@ func (s *Session) Db(opts ...sqlx.Option) (*sqlx.Service, error) { } func (s *Session) Stater() *state.Service { - return state.New(s.stater) + return state.New(s.injector) } func (s *Session) FlushTemplate(ctx context.Context) error { @@ -148,8 +148,8 @@ func WithMessageBus(messageBusses []*mbus.Resource) Option { } } -func WithStater(stater state.Stater) Option { +func WithStater(injector state.Injector) Option { return func(s *Session) { - s.stater = stater + s.injector = injector } } diff --git a/service/executor/extension/validator.go b/service/executor/extension/validator.go index 8c23f0df4..740b16f37 100644 --- a/service/executor/extension/validator.go +++ b/service/executor/extension/validator.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" "fmt" + + derrors "github.com/viant/datly/utils/errors" "github.com/viant/datly/utils/httputils" "github.com/viant/govalidator" sqlxvalidator "github.com/viant/sqlx/io/validator" @@ -33,6 +35,9 @@ func (v *SqlxValidator) Validate(ctx context.Context, any interface{}, opts ...v err = v.validator.validateWithSqlx(ctx, any, validation, options) } if err != nil { + if derrors.IsDatabaseError(err) { + return validation, err + } validation.Append("/", "", "", "error", err.Error()) } return validation, nil @@ -46,9 +51,15 @@ func (v *Validator) Validate(ctx context.Context, any interface{}, opts ...valid validation := getOrCreateValidation(options) err := v.validateWithGoValidator(ctx, any, validation, options) if err != nil { + if derrors.IsDatabaseError(err) { + return validation, err + } validation.Append("/", "", "", "error", err.Error()) } if err = v.validateWithSqlx(ctx, any, validation, options); err != nil { + if derrors.IsDatabaseError(err) { + return validation, err + } validation.Append("/", "", "", "error", err.Error()) } return validation, nil diff --git a/service/executor/handler/executor.go b/service/executor/handler/executor.go index 4d5e719ae..70f1746ba 100644 --- a/service/executor/handler/executor.go +++ b/service/executor/handler/executor.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" "fmt" + "net/http" + "github.com/viant/datly/repository" "github.com/viant/datly/repository/contract" executor "github.com/viant/datly/service/executor" @@ -20,7 +22,6 @@ import ( "github.com/viant/xdatly/handler/sqlx" hstate "github.com/viant/xdatly/handler/state" "github.com/viant/xdatly/handler/validator" - "net/http" ) type ( @@ -94,6 +95,12 @@ func (e *Executor) Session(ctx context.Context) (*executor.Session, error) { e.executorSession = sess sess.SessionHandler = sessionHandler + // inherit tx from session options if available + if e.tx == nil { + if tx := e.session.Options.SqlTx(); tx != nil { + e.tx = tx + } + } return e.executorSession, err } @@ -126,6 +133,9 @@ func (e *Executor) newSession(aSession *session.Session, opts ...Option) *extens if options.auth != nil { e.auth = options.auth } + if e.logger == nil { + e.logger = options.logger + } res := e.view.GetResource() sess := extension.NewSession( extension.WithTemplateFlush(func(ctx context.Context) error { @@ -135,6 +145,7 @@ func (e *Executor) newSession(aSession *session.Session, opts ...Option) *extens extension.WithRedirect(e.redirect), extension.WithSql(e.newSqlService), extension.WithHttp(e.newHttp), + extension.WithLogger(e.logger), extension.WithAuth(e.newAuth), extension.WithMessageBus(res.MessageBuses), ) @@ -157,6 +168,10 @@ func (e *Executor) newSqlService(options *sqlx.Options) (sqlx.Sqlx, error) { if unit == e.dataUnit { //we are using View that can contain SQL Statements in Velty txStartedNotifier = e.txStarted } + // default SQLx tx to executor tx to avoid internal Begin/Commit if caller provided one + if options.WithTx == nil && e.tx != nil { + options.WithTx = e.tx + } return &Service{ txNotifier: txStartedNotifier, dataUnit: unit, @@ -168,6 +183,7 @@ func (e *Executor) newSqlService(options *sqlx.Options) (sqlx.Sqlx, error) { } func (e *Executor) getDataUnit(options *sqlx.Options) (*expand.DataUnit, error) { + e.ensureConnectors() if (options.WithDb == nil && options.WithTx == nil) && options.WithConnector == e.view.Connector.Name { return e.dataUnit, nil } @@ -192,6 +208,11 @@ func (e *Executor) getDataUnit(options *sqlx.Options) (*expand.DataUnit, error) if connector == nil { return nil, fmt.Errorf("failed to lookup connector %v", options.WithConnector) } + + if _, ok := e.connectors[options.WithConnector]; !ok { + e.connectors[options.WithConnector] = connector + } + db, err := connector.DB() if err != nil { return nil, err @@ -206,6 +227,17 @@ func (e *Executor) getDataUnit(options *sqlx.Options) (*expand.DataUnit, error) return e.dataUnit, nil } +func (e *Executor) ensureConnectors() { + if len(e.connectors) == 0 { + e.connectors = make(view.Connectors) + if res := e.view.GetResource(); res != nil { + for _, connector := range res.Connectors { + e.connectors[connector.Name] = connector + } + } + } +} + func (e *Executor) Execute(ctx context.Context) error { if e.executed { return nil @@ -217,6 +249,10 @@ func (e *Executor) Execute(ctx context.Context) error { dbOptions = append(dbOptions, executor.WithTx(e.tx)) } + err := service.ExecuteStmts(ctx, executor.NewViewDBSource(e.view), newSqlxIterator(e.dataUnit.Statements.Executable), dbOptions...) + if err != nil { + return err + } for _, unit := range e.dataUnits { dbSource := &DbSource{} dbSource.db, _ = unit.MetaSource.Db() @@ -225,7 +261,7 @@ func (e *Executor) Execute(ctx context.Context) error { } } - return service.ExecuteStmts(ctx, executor.NewViewDBSource(e.view), newSqlxIterator(e.dataUnit.Statements.Executable), dbOptions...) + return err } func (e *Executor) ExpandAndExecute(ctx context.Context) (*executor.Session, error) { @@ -262,7 +298,6 @@ func (e *Executor) redirect(ctx context.Context, route *http2.Route, opts ...hst request.Header = originalRequest.Header } stateOptions := hstate.NewOptions(opts...) - unmarshal := aComponent.UnmarshalFunc(request) locatorOptions := append(aComponent.LocatorOptions(request, hstate.NewForm(), unmarshal)) if stateOptions.Query() != nil { @@ -286,8 +321,13 @@ func (e *Executor) redirect(ctx context.Context, route *http2.Route, opts ...hst session.WithOperate(e.session.Options.Operate()), session.WithTypes(&aComponent.Contract.Input.Type, &aComponent.Contract.Output.Type), session.WithComponent(aComponent), + session.WithLogger(e.logger), session.WithRegistry(registry), ) + if tx := stateOptions.SqlTx(); tx != nil { + // associate tx with session; child executor will reuse it + aSession.Apply(session.WithSQLTx(tx)) + } err = aSession.InitKinds(state.KindComponent, state.KindHeader, state.KindRequestBody, state.KindForm, state.KindQuery) if err != nil { @@ -295,7 +335,11 @@ func (e *Executor) redirect(ctx context.Context, route *http2.Route, opts ...hst } ctx = aSession.Context(ctx, true) anExecutor := NewExecutor(aComponent.View, aSession) - return anExecutor.NewHandlerSession(ctx) + // ensure Execute(ctx) uses the provided tx (avoid autocommit) + if tx := stateOptions.SqlTx(); tx != nil { + anExecutor.tx = tx + } + return anExecutor.NewHandlerSession(ctx, WithLogger(aSession.Logger())) } func (e *Executor) newHttp() http2.Http { diff --git a/service/executor/handler/locator/handler.go b/service/executor/handler/locator/handler.go index 3758b8abd..6f0c31aeb 100644 --- a/service/executor/handler/locator/handler.go +++ b/service/executor/handler/locator/handler.go @@ -11,6 +11,7 @@ import ( "github.com/viant/datly/view/state" "github.com/viant/datly/view/state/kind" "github.com/viant/datly/view/state/kind/locator" + "reflect" ) type Handler struct { @@ -22,7 +23,7 @@ func (v *Handler) Names() []string { return nil } -func (v *Handler) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (v *Handler) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { resource := v.options.Resource if resource == nil { return nil, false, fmt.Errorf("failed to lookup handler resource: %v", name) diff --git a/service/executor/handler/options.go b/service/executor/handler/options.go index 538e130df..e1221318b 100644 --- a/service/executor/handler/options.go +++ b/service/executor/handler/options.go @@ -4,6 +4,7 @@ import ( "embed" "github.com/viant/datly/service/auth" "github.com/viant/datly/view/state" + "github.com/viant/xdatly/handler/logger" ) type options struct { @@ -11,6 +12,7 @@ type options struct { embedFS *embed.FS opts []Option auth *auth.Service + logger logger.Logger } func (o *options) Clone(opts []Option) *options { @@ -37,6 +39,12 @@ func WithTypes(types ...*state.Type) Option { } } +func WithLogger(logger logger.Logger) Option { + return func(o *options) { + o.logger = logger + } +} + func WithAuth(auth *auth.Service) Option { return func(o *options) { o.auth = auth diff --git a/service/executor/service.go b/service/executor/service.go index c89e8fc41..086a40695 100644 --- a/service/executor/service.go +++ b/service/executor/service.go @@ -4,6 +4,11 @@ import ( "context" "database/sql" "fmt" + "reflect" + "strings" + "sync/atomic" + "time" + "github.com/viant/datly/logger" expand2 "github.com/viant/datly/service/executor/expand" vsession "github.com/viant/datly/service/session" @@ -13,8 +18,6 @@ import ( "github.com/viant/sqlx/option" "github.com/viant/xdatly/handler/exec" "github.com/viant/xdatly/handler/response" - "reflect" - "time" ) type ( @@ -31,6 +34,8 @@ type ( dbSource DBSource collections map[string]*batcher.Collection logger *logger.Adapter + inserted int32 + updated int32 } DBOption func(options *DBOptions) @@ -190,6 +195,9 @@ func (e *Executor) handleUpdate(ctx context.Context, sess *dbSession, db *sql.DB options = append(options, db) updated, err := service.Exec(ctx, executable.Data, options...) + if err == nil { + atomic.AddInt32(&sess.updated, int32(updated)) + } e.logMetrics(ctx, executable.Table, "UPDATE", updated, now, err) return err } @@ -212,7 +220,7 @@ func (e *Executor) logMetrics(ctx context.Context, table string, operation strin if err != nil { metric.Error = err.Error() } - value.(*exec.Context).Metrics.Append(&metric) + value.(*exec.Context).AppendMetrics(&metric) } func (e *Executor) handleInsert(ctx context.Context, sess *dbSession, executable *expand2.Executable, db *sql.DB) error { @@ -233,6 +241,9 @@ func (e *Executor) handleInsert(ctx context.Context, sess *dbSession, executable } options = append(options, tx) inserted, _, err = service.Exec(ctx, executable.Data, options...) + if err == nil { + atomic.AddInt32(&sess.inserted, int32(inserted)) + } e.logMetrics(ctx, executable.Table, "INSERT", inserted, started, err) return err } @@ -252,6 +263,23 @@ func (e *Executor) handleInsert(ctx context.Context, sess *dbSession, executable options = append(options, option.BatchSize(batchSize)) options = append(options, e.dbOptions(db, sess)) inserted, _, err = service.Exec(ctx, executable.Data, options...) + if err == nil { + atomic.AddInt32(&sess.inserted, int32(inserted)) + } + isInvalidConnection := err != nil && strings.Contains(err.Error(), "invalid connection") + if isInvalidConnection && atomic.LoadInt32(&sess.inserted) == 0 && atomic.LoadInt32(&sess.updated) == 0 { + var dErr error + db, dErr = sess.dbSource.Db(ctx) + if dErr != nil { + return fmt.Errorf("failed after retry: %w", err) + } + sess.tx.db = db + sess.tx.tx = nil + if _, err = sess.tx.Tx(); err != nil { + return err + } + inserted, _, err = service.Exec(ctx, executable.Data, options...) + } e.logMetrics(ctx, executable.Table, "INSERT", inserted, started, err) return err } diff --git a/service/operator/executor.go b/service/operator/executor.go index 94ba9c1b6..94d7fab94 100644 --- a/service/operator/executor.go +++ b/service/operator/executor.go @@ -3,12 +3,13 @@ package operator import ( "context" "fmt" + "time" + "github.com/viant/datly/repository" "github.com/viant/datly/repository/contract" "github.com/viant/datly/service/executor/handler" "github.com/viant/gmetric/counter" xhandler "github.com/viant/xdatly/handler" - "time" "github.com/viant/datly/service/session" "github.com/viant/datly/view/state/kind/locator" @@ -25,6 +26,7 @@ func (s *Service) execute(ctx context.Context, aComponent *repository.Component, if aComponent.Handler != nil { aSession.SetView(aComponent.View) sessionHandler, err := anExecutor.NewHandlerSession(ctx, + handler.WithLogger(aSession.Logger()), handler.WithTypes(aComponent.Types()...), handler.WithAuth(aSession.Auth())) if err != nil { return nil, err @@ -59,6 +61,8 @@ func (s *Service) execute(ctx context.Context, aComponent *repository.Component, status := contract.StatusSuccess(executorSession.TemplateState) if err := aSession.SetState(ctx, aComponent.Output.Type.Parameters, responseState, aSession.Indirect(true, locator.WithCustom(&status), + locator.WithLogger(aSession.Logger()), + locator.WithState(statelet.Template))); err != nil { return nil, fmt.Errorf("failed to set response %w", err) } diff --git a/service/operator/reader.go b/service/operator/reader.go index bca67d847..801638e12 100644 --- a/service/operator/reader.go +++ b/service/operator/reader.go @@ -23,6 +23,10 @@ func (s *Service) runQuery(ctx context.Context, component *repository.Component, defer func() { if r := recover(); r != nil { panicMsg := fmt.Sprintf("Panic occurred: %v, Stack trace: %v", r, string(debug.Stack())) + logger := aSession.Logger() + if logger == nil { + panic(panicMsg) + } aSession.Logger().Errorc(ctx, panicMsg) err = response.NewError(http.StatusInternalServerError, "Internal server error") output = nil @@ -40,7 +44,7 @@ func (s *Service) runQuery(ctx context.Context, component *repository.Component, if err := s.updateJobStatusDone(ctx, component, handlerResponse, setting.SyncFlag, startTime); err != nil { return nil, err } - if output, err = s.finalize(ctx, handlerResponse.Output, handlerResponse.Error); err != nil { + if output, err = s.finalize(ctx, handlerResponse.Output, handlerResponse.Error, aSession); err != nil { aSession.ClearCache(component.Output.Type.Parameters) return s.HandleError(ctx, aSession, component, err) } diff --git a/service/operator/service.go b/service/operator/service.go index bf50ece0b..a5a61ecb1 100644 --- a/service/operator/service.go +++ b/service/operator/service.go @@ -6,11 +6,16 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "reflect" + "time" + "github.com/viant/afs" "github.com/viant/afs/file" "github.com/viant/datly/repository" rasync "github.com/viant/datly/repository/async" "github.com/viant/datly/repository/content" + "github.com/viant/datly/repository/contract" "github.com/viant/datly/service" "github.com/viant/datly/service/reader" "github.com/viant/datly/service/session" @@ -25,13 +30,12 @@ import ( xhandler "github.com/viant/xdatly/handler" "github.com/viant/xdatly/handler/async" "github.com/viant/xdatly/handler/exec" + xhttp "github.com/viant/xdatly/handler/http" "github.com/viant/xdatly/handler/logger" "github.com/viant/xdatly/handler/response" hstate "github.com/viant/xdatly/handler/state" + xstate "github.com/viant/xdatly/handler/state" "google.golang.org/api/googleapi" - "net/http" - "reflect" - "time" ) type Service struct { @@ -84,6 +88,7 @@ func (s *Service) HandleError(ctx context.Context, aSession *session.Session, aC func (s *Service) operate(ctx context.Context, aComponent *repository.Component, aSession *session.Session) (interface{}, error) { var err error + ctx, err = s.EnsureContext(ctx, aSession, aComponent) if err != nil { return nil, err @@ -118,13 +123,26 @@ func (s *Service) operate(ctx context.Context, aComponent *repository.Component, } } - return s.finalize(ctx, ret, err) + return s.finalize(ctx, ret, err, aSession) } return nil, response.NewError(500, fmt.Sprintf("unsupported Type %v", aComponent.Service)) } -func (s *Service) finalize(ctx context.Context, ret interface{}, err error) (interface{}, error) { +func (s *Service) finalize(ctx context.Context, ret interface{}, err error, aSession *session.Session) (interface{}, error) { + if injectorFinalizer, ok := ret.(state.InjectorFinalizer); ok { + + lookup := func(ctx context.Context, route xhttp.Route) (xstate.Injector, error) { + aComponent, err := aSession.Registry().Lookup(ctx, contract.NewPath(route.Method, route.URL)) + if err != nil { + return nil, err + } + return aSession.NewSession(aComponent), nil + } + + err = injectorFinalizer.Finalize(ctx, lookup) + return ret, err + } if err != nil { return ret, err } diff --git a/service/reader/handler/handler.go b/service/reader/handler/handler.go index 83d47d11e..6e83335e3 100644 --- a/service/reader/handler/handler.go +++ b/service/reader/handler/handler.go @@ -2,7 +2,8 @@ package handler import ( "context" - goJson "github.com/goccy/go-json" + "encoding/json" + "github.com/viant/datly/gateway/router/status" _ "github.com/viant/datly/repository/locator/async" _ "github.com/viant/datly/repository/locator/component" @@ -10,6 +11,9 @@ import ( _ "github.com/viant/datly/repository/locator/output" _ "github.com/viant/datly/service/executor/handler/locator" + "net/http" + "reflect" + reader "github.com/viant/datly/service/reader" "github.com/viant/datly/service/session" "github.com/viant/datly/utils/httputils" @@ -18,8 +22,6 @@ import ( "github.com/viant/datly/view/state/kind/locator" "github.com/viant/structology" "github.com/viant/xdatly/handler/response" - "net/http" - "reflect" ) type ( @@ -65,7 +67,9 @@ func (h *Handler) Handle(ctx context.Context, aView *view.View, aSession *sessio resultState := h.output.NewState() statelet := aSession.State().Lookup(aView) - var locatorOptions []locator.Option + var locatorOptions = []locator.Option{ + locator.WithLogger(aSession.Logger()), + } locatorOptions = append(locatorOptions, locator.WithParameterLookup(func(ctx context.Context, parameter *state.Parameter) (interface{}, bool, error) { return aSession.LookupValue(ctx, parameter, aSession.Indirect(true, locatorOptions...)) }), @@ -135,7 +139,11 @@ func (h *Handler) publishViewSummaryIfNeeded(aView *view.View, ret *Response) { if templateMeta.Kind != view.MetaKindHeader { return } - data, err := goJson.Marshal(ret.Reader.DataSummary) + var data []byte + var err error + if ret.Reader.DataSummary != nil { + data, err = json.Marshal(ret.Reader.DataSummary) + } if err != nil { ret.StatusCode = http.StatusInternalServerError ret.Status.Status = "error" @@ -153,7 +161,7 @@ func (h *Handler) publishMetricsIfNeeded(aSession *reader.Session, ret *Response if info.Executions == nil { continue } - data, err := goJson.Marshal(info) + data, err := json.Marshal(info) if err != nil { continue } diff --git a/service/reader/service.go b/service/reader/service.go index 29f1fea7d..1fda9fc9e 100644 --- a/service/reader/service.go +++ b/service/reader/service.go @@ -4,6 +4,13 @@ import ( "context" "database/sql" "fmt" + "reflect" + "strings" + "sync" + "sync/atomic" + "time" + "unsafe" + "github.com/google/uuid" "github.com/viant/datly/service/executor/expand" "github.com/viant/datly/shared" @@ -19,10 +26,6 @@ import ( "github.com/viant/xdatly/handler" "github.com/viant/xdatly/handler/exec" "github.com/viant/xdatly/handler/response" - "reflect" - "sync" - "time" - "unsafe" ) // Service represents reader service @@ -101,7 +104,7 @@ func (s *Service) afterRead(ctx context.Context, aSession *Session, collector *v onFinish(end) if value := ctx.Value(exec.ContextKey); value != nil { if exeCtx := value.(*exec.Context); exeCtx != nil { - exeCtx.Metrics.Append(metrics) + exeCtx.AppendMetrics(metrics) } } } @@ -183,6 +186,7 @@ func (s *Service) readAll(ctx context.Context, session *Session, collector *view } return } + // if onRelationalConcurrency > 1 , then only we call it concurrently concurrencyLimit := make(chan struct{}, onRelationerConcurrency) var onRelationWaitGroup sync.WaitGroup @@ -513,12 +517,25 @@ func (s *Service) queryWithHandler(ctx context.Context, session *Session, aView if session.DryRun { return []*response.SQLExecution{stats}, nil } + + retires := uint32(0) +BEGIN: reader, err := read.New(ctx, db, parametrizedSQL.SQL, collector.NewItem(), options...) + + isInvalidConnection := err != nil && strings.Contains(err.Error(), "invalid connection") + if isInvalidConnection && atomic.AddUint32(&retires, 1) < 3 { + db, err = aView.Connector.DB() + if err != nil { + return nil, fmt.Errorf("failed to connect to db: %w", err) + } + goto BEGIN + } if err != nil { stats.SetError(err) anExec, err := s.HandleSQLError(err, session, aView, parametrizedSQL, stats) return []*response.SQLExecution{anExec}, err } + defer func() { stmt := reader.Stmt() if stmt == nil { @@ -527,7 +544,17 @@ func (s *Service) queryWithHandler(ctx context.Context, session *Session, aView _ = stmt.Close() }() err = reader.QueryAll(ctx, handler, parametrizedSQL.Args...) + + isInvalidConnection = err != nil && strings.Contains(err.Error(), "invalid connection") + if isInvalidConnection && atomic.AddUint32(&retires, 1) < 3 { + db, err = aView.Connector.DB() + if err != nil { + return nil, fmt.Errorf("failed to connect to db: %w", err) + } + goto BEGIN + } end := time.Now() + aView.Logger.ReadingData(end.Sub(begin), parametrizedSQL.SQL, *readData, parametrizedSQL.Args, err) if err != nil { stats.SetError(err) diff --git a/service/reader/sql.go b/service/reader/sql.go index 33d7747ca..256cd26f3 100644 --- a/service/reader/sql.go +++ b/service/reader/sql.go @@ -3,14 +3,15 @@ package reader import ( "context" "fmt" + "strconv" + "strings" + "github.com/viant/datly/service/executor/expand" "github.com/viant/datly/service/reader/metadata" "github.com/viant/datly/shared" "github.com/viant/datly/view" "github.com/viant/datly/view/keywords" "github.com/viant/sqlx/io/read/cache" - "strconv" - "strings" ) const ( @@ -44,19 +45,36 @@ func (b *Builder) Build(ctx context.Context, opts ...BuilderOption) (*cache.Parm options := newBuilderOptions(opts...) aView := options.view statelet := options.statelet - batchData := *options.batchData + // guard against nil batchData passed by callers + var batchData view.BatchData + if options.batchData != nil { + batchData = *options.batchData + } relation := options.relation exclude := options.exclude parent := options.parent partitions := options.partition expander := options.expander + + // ensure non-nil statelet to avoid nil deref on Template usage + if statelet == nil { + statelet = view.NewStatelet() + statelet.Init(aView) + } + state, err := aView.Template.EvaluateSource(ctx, statelet.Template, parent, &batchData, expander) if err != nil { return nil, err } + if state == nil { + return nil, fmt.Errorf("failed to evaluate state for view %v, state was nil", aView.Name) + } + if state.Expanded == "" { + return nil, fmt.Errorf("failed to evaluate expanded for view %vm statelet was nil", aView.Name) + } if len(state.Filters) > 0 { - statelet.Filters = append(statelet.Filters, state.Filters...) + statelet.AppendFilters(state.Filters) } if aView.Template.IsActualTemplate() && aView.ShouldTryDiscover() { state.Expanded = metadata.EnrichWithDiscover(state.Expanded, true) @@ -323,7 +341,7 @@ func (b *Builder) updateColumnsIn(params *view.CriteriaParam, batchData *view.Ba params.ColumnsIn = sb.String() } -func (b *Builder) appendOrderBy(sb *strings.Builder, view *view.View, selector *view.Statelet) error { +func (b *Builder) appendOrderBy(sb *strings.Builder, aView *view.View, selector *view.Statelet) error { if selector.OrderBy != "" { fragment := strings.Builder{} items := strings.Split(strings.ReplaceAll(selector.OrderBy, ":", " "), ",") @@ -344,12 +362,23 @@ func (b *Builder) appendOrderBy(sb *strings.Builder, view *view.View, selector * switch strings.ToLower(sortDirection) { case "asc", "desc", "": default: - return fmt.Errorf("invalid sort direction %v for column %v at view %v", sortDirection, column, view.Name) + return fmt.Errorf("invalid sort direction %v for column %v at aView %v", sortDirection, column, aView.Name) } - col, ok := view.ColumnByName(column) + col, ok := aView.ColumnByName(column) + if !ok { + + if aView.Selector.Constraints.HasOrderByColumn(column) { + mapped := aView.Selector.Constraints.OrderByColumn[column] + col = &view.Column{ + Name: mapped, + } + ok = true + } + + } if !ok { - return fmt.Errorf("not found column %v at view %v", column, view.Name) + return fmt.Errorf("not found column %v at aView %v", column, aView.Name) } fragment.WriteString(col.Name) if sortDirection != "" { @@ -362,9 +391,9 @@ func (b *Builder) appendOrderBy(sb *strings.Builder, view *view.View, selector * return nil } - if view.Selector.OrderBy != "" { + if aView.Selector.OrderBy != "" { sb.WriteString(orderByFragment) - sb.WriteString(strings.ReplaceAll(view.Selector.OrderBy, ":", " ")) + sb.WriteString(strings.ReplaceAll(aView.Selector.OrderBy, ":", " ")) return nil } diff --git a/service/session/option.go b/service/session/option.go index 0fc7ea6a0..3568b7b35 100644 --- a/service/session/option.go +++ b/service/session/option.go @@ -2,7 +2,9 @@ package session import ( "context" + "database/sql" "embed" + "github.com/viant/datly/repository" "github.com/viant/datly/service/auth" "github.com/viant/datly/view" @@ -32,6 +34,8 @@ type ( scope string embeddedFS *embed.FS auth *auth.Service + preseedCache bool + sqlTx *sql.Tx } Option func(o *Options) @@ -45,6 +49,11 @@ func (o *Options) Registry() *repository.Registry { return o.registry } +// SqlTx returns associated SQL transaction (if any) +func (o *Options) SqlTx() *sql.Tx { + return o.sqlTx +} + func (o *Options) HasInputParameters() bool { if o.locatorOpt == nil { return false @@ -154,6 +163,20 @@ func WithAuth(auth *auth.Service) Option { } } +// WithSQLTx associates an existing SQL transaction with the session +func WithSQLTx(tx *sql.Tx) Option { + return func(s *Options) { + s.sqlTx = tx + } +} + +// WithPreseedCache controls whether NewSession should pre-seed child cache from parent (default false) +func WithPreseedCache(flag bool) Option { + return func(s *Options) { + s.preseedCache = flag + } +} + func WithComponent(component *repository.Component) Option { return func(s *Options) { s.component = component @@ -183,3 +206,9 @@ func WithRegistry(registry *repository.Registry) Option { s.registry = registry } } + +func WithLogger(logger logger.Logger) Option { + return func(s *Options) { + s.logger = logger + } +} diff --git a/service/session/selector.go b/service/session/selector.go index 895a953d2..b6ff7c140 100644 --- a/service/session/selector.go +++ b/service/session/selector.go @@ -3,13 +3,14 @@ package session import ( "context" "fmt" + "strconv" + "strings" + "github.com/viant/datly/service/session/criteria" "github.com/viant/datly/view" "github.com/viant/tagly/format/text" "github.com/viant/xdatly/codec" "github.com/viant/xdatly/handler/response" - "strconv" - "strings" ) func (s *Session) setQuerySelector(ctx context.Context, ns *view.NamespaceView, opts *Options) (err error) { @@ -18,6 +19,14 @@ func (s *Session) setQuerySelector(ctx context.Context, ns *view.NamespaceView, return nil } + selector := s.state.Lookup(ns.View) + + if opts != nil && opts.locatorOpt != nil && opts.locatorOpt.QuerySelectors != nil { //override selector + querySelectors := opts.locatorOpt.QuerySelectors + if namedSelector := querySelectors.Find(ns.View.Name); namedSelector != nil { + selector.QuerySelector = namedSelector.QuerySelector + } + } if err = s.populateFieldQuerySelector(ctx, ns, opts); err != nil { return response.NewParameterError(ns.View.Name, selectorParameters.FieldsParameter.Name, err) } @@ -36,7 +45,6 @@ func (s *Session) setQuerySelector(ctx context.Context, ns *view.NamespaceView, if err = s.populatePageQuerySelector(ctx, ns, opts); err != nil { return response.NewParameterError(ns.View.Name, selectorParameters.PageParameter.Name, err) } - selector := s.state.Lookup(ns.View) if selector.Limit == 0 && selector.Offset != 0 { return fmt.Errorf("can't use offset without limit - view: %v", ns.View.Name) } @@ -154,6 +162,9 @@ func (s *Session) setOrderByQuerySelector(value interface{}, ns *view.NamespaceV continue //position based, not need to validate } + if ns.View.Selector.Constraints.HasOrderByColumn(column) { + continue + } _, ok := ns.View.ColumnByName(column) if !ok { return fmt.Errorf("not found column %v at view %v", items, ns.View.Name) @@ -201,7 +212,10 @@ func (s *Session) setLimitQuerySelector(value interface{}, ns *view.NamespaceVie return fmt.Errorf("can't use Limit on view %v", ns.View.Name) } selector := s.state.Lookup(ns.View) - limit := value.(int) + limit, err := toInt(value) + if err != nil { + return fmt.Errorf("invalid limit value: %v", err) + } if limit <= ns.View.Selector.Limit || ns.View.Selector.Limit == 0 { selector.Limit = limit } @@ -223,7 +237,19 @@ func (s *Session) setFieldsQuerySelector(value interface{}, ns *view.NamespaceVi return fmt.Errorf("can't use projection on view %v", ns.View.Name) } selector := s.state.Lookup(ns.View) - fields := value.([]string) + var fields []string + switch v := value.(type) { + case []string: + fields = v + case []interface{}: + for _, elem := range v { + text, ok := elem.(string) + if !ok { + continue + } + fields = append(fields, text) + } + } for _, field := range fields { fieldName := ns.View.CaseFormat.Format(field, text.CaseFormatUpperCamel) if err = canUseColumn(ns.View, fieldName); err != nil { @@ -270,3 +296,20 @@ func canUseColumn(aView *view.View, columnName string) error { } return nil } + +func toInt(v interface{}) (int, error) { + switch val := v.(type) { + case int: + return val, nil + case int32: + return int(val), nil + case int64: + return int(val), nil + case float64: + return int(val), nil + case float32: + return int(val), nil + default: + return 0, fmt.Errorf("unsupported type: %T", v) + } +} diff --git a/service/session/state.go b/service/session/state.go index 62a75f07f..7c2ff10df 100644 --- a/service/session/state.go +++ b/service/session/state.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/viant/datly/internal/converter" + "github.com/viant/datly/repository" "github.com/viant/datly/service/auth" "github.com/viant/datly/utils/types" "github.com/viant/datly/view" @@ -42,6 +43,42 @@ type ( } ) +func (s *Session) NewSession(component *repository.Component) *Session { + ret := *s + // set component and view on the child session (do not mutate receiver) + ret.component = component + ret.Options.component = component + ret.view = component.View + if ret.locatorOpt != nil { + if _, ok := ret.locatorOpt.Views[component.View.Name]; !ok { + ret.locatorOpt.Views.Register(component.View) + } + } + + // create a fresh cache and optionally pre-populate from parent cache values + parent := s.cache + ret.cache = newCache() + if ret.Options.preseedCache && parent != nil { + parent.RWMutex.RLock() + for k, v := range parent.values { + ret.cache.values[k] = v + } + parent.RWMutex.RUnlock() + } + + // reset predicates (filters) on the child session state + if ret.Options.state != nil { + ret.Options.state.RWMutex.Lock() + for _, st := range ret.Options.state.Views { + if st != nil { + st.Filters = nil + } + } + ret.Options.state.RWMutex.Unlock() + } + return &ret +} + func (s *Session) SetView(view *view.View) { s.view = view } @@ -168,6 +205,7 @@ func (s *Session) viewLookupOptions(aView *view.View, parameters state.NamedPara if !opts.HasInputParameters() { result = append(result, locator.WithInputParameters(parameters)) } + result = append(result, locator.WithLogger(s.logger)) result = append(result, locator.WithReadInto(s.ReadInto)) viewState := s.state.Lookup(aView) result = append(result, locator.WithState(viewState.Template)) @@ -247,6 +285,52 @@ func (s *Session) populateParameterInBackground(ctx context.Context, parameter * } } +// The function below causes SIGBUS when template parameters are rebound. +//E.g. a predicate builder velty expression is located in an embedded SQL, outside main DQL +//func (s *Session) populateParameter(ctx context.Context, parameter *state.Parameter, aState *structology.State, options *Options) error { +// value, has, err := s.LookupValue(ctx, parameter, options) +// if err != nil { +// return err +// } +// if !has { +// if parameter.IsRequired() { +// return fmt.Errorf("parameter %v is required", parameter.Name) +// } +// return nil +// } +// +// parameterSelector := parameter.Selector() +// if options.indirectState || parameterSelector == nil { //p +// parameterSelector, err = aState.Selector(parameter.Name) +// if parameterSelector == nil { +// switch parameter.In.Kind { +// case state.KindConst: +// return nil +// } +// } +// if err != nil { +// return err +// } +// } +// +// if value, err = s.ensureValidValue(value, parameter, parameterSelector, options); err != nil { +// return err +// } +// err = parameterSelector.SetValue(aState.Pointer(), value) +// +// //ensure last written can be shared +// if err == nil { +// +// switch parameterSelector.Type().Kind() { +// case reflect.Ptr: +// if parameter.Schema.Type() == parameterSelector.Type() { +// s.cache.put(parameter, parameterSelector.Value(aState.Pointer())) +// } +// } +// } +// return err +//} + func (s *Session) populateParameter(ctx context.Context, parameter *state.Parameter, aState *structology.State, options *Options) error { value, has, err := s.LookupValue(ctx, parameter, options) if err != nil { @@ -258,29 +342,25 @@ func (s *Session) populateParameter(ctx context.Context, parameter *state.Parame } return nil } - parameterSelector := parameter.Selector() - if options.indirectState || parameterSelector == nil { //p - parameterSelector, err = aState.Selector(parameter.Name) - if parameterSelector == nil && parameter.In.Kind == state.KindConst { // TODO do we really need it? - return nil - } - if err != nil { - return err - } + + // Resolve selector strictly from the state's layout + // Treat "not found" as a no-op (skip), since this view doesn't declare that parameter. + parameterSelector, err := aState.Selector(parameter.Name) + if err != nil || parameterSelector == nil { + return nil } + if value, err = s.ensureValidValue(value, parameter, parameterSelector, options); err != nil { return err } - err = parameterSelector.SetValue(aState.Pointer(), value) + if err = parameterSelector.SetValue(aState.Pointer(), value); err != nil { + return err + } - //ensure last written can be shared - if err == nil { - switch parameterSelector.Type().Kind() { - case reflect.Ptr: - s.cache.put(parameter, parameterSelector.Value(aState.Pointer())) - } + if parameterSelector.Type().Kind() == reflect.Ptr { + s.cache.put(parameter, parameterSelector.Value(aState.Pointer())) } - return err + return nil } func (s *Session) canRead(ctx context.Context, parameter *state.Parameter, opts *Options) (bool, error) { @@ -346,16 +426,22 @@ func (s *Session) ensureValidValue(value interface{}, parameter *state.Parameter if valueType.Elem().Kind() == reflect.Struct && parameter.Schema.Type().Kind() == reflect.Slice { if parameter.Schema.CompType() == valueType { sliceValuePtr := reflect.New(parameterType) + + if isNil(value) { + empty := reflect.MakeSlice(parameterType, 0, 0) + sliceValuePtr.Elem().Set(empty) + return sliceValuePtr.Interface(), nil // []T{} + } + sliceValue := reflect.MakeSlice(parameterType, 1, 1) sliceValuePtr.Elem().Set(sliceValue) sliceValue.Index(0).Set(reflect.ValueOf(value)) - return sliceValuePtr.Interface(), nil + return sliceValuePtr.Interface(), nil // []T{value}` } } case reflect.Slice: - ptr := xunsafe.AsPointer(value) - slice := parameter.Schema.Slice() - sliceLen := slice.Len(ptr) + rSlice := reflect.ValueOf(value) + sliceLen := rSlice.Len() if errorMessage := validateSliceParameter(parameter, sliceLen); errorMessage != "" { return nil, errors.New(errorMessage) } @@ -366,11 +452,44 @@ func (s *Session) ensureValidValue(value interface{}, parameter *state.Parameter default: switch sliceLen { case 0: - value = reflect.New(parameter.OutputType().Elem()).Elem().Interface() + switch outputType.Kind() { + case reflect.Ptr: + value = reflect.New(outputType.Elem()).Elem().Interface() + case reflect.Struct: + value = reflect.New(outputType).Elem().Interface() + default: + value = reflect.New(outputType).Elem().Interface() + } valueType = reflect.TypeOf(value) case 1: - value = slice.ValuePointerAt(ptr, 0) - valueType = reflect.TypeOf(value) + elem := rSlice.Index(0) + rawType := elem.Type() + if rawType.Kind() == reflect.Ptr { + rawType = rawType.Elem() + } + if rawType.Kind() == reflect.Interface { + rawType = rawType.Elem() + } + + if rawType.Kind() != reflect.Struct { + break + } + + if elem.Kind() == reflect.Interface && !elem.IsNil() { + elem = elem.Elem() + } + if elem.Kind() == reflect.Ptr { + value = elem.Interface() + valueType = elem.Type() + break + } + if elem.CanAddr() { + value = elem.Addr().Interface() + valueType = elem.Addr().Type() + break + } + value = elem.Interface() + valueType = elem.Type() default: return nil, fmt.Errorf("parameter %v return more than one value, len: %v rows ", parameter.Name, sliceLen) } @@ -388,53 +507,55 @@ func (s *Session) ensureValidValue(value interface{}, parameter *state.Parameter } if parameter.Schema.IsStruct() && !(valueType == selector.Type() || valueType.ConvertibleTo(selector.Type()) || valueType.AssignableTo(selector.Type())) { - - rawSelectorType := selector.Type() - isSelectorPtr := false - if rawSelectorType.Kind() == reflect.Ptr { - rawSelectorType = rawSelectorType.Elem() - isSelectorPtr = true + destType := selector.Type() + rawDestType := destType + destIsPtr := false + if rawDestType.Kind() == reflect.Ptr { + rawDestType = rawDestType.Elem() + destIsPtr = true } - isValuePtr := false - rawValueType := valueType - if rawValueType.Kind() == reflect.Ptr { - rawValueType = valueType.Elem() - isValuePtr = true + + rawSrcType := valueType + srcIsPtr := false + if rawSrcType.Kind() == reflect.Ptr { + rawSrcType = rawSrcType.Elem() + srcIsPtr = true } - if rawSelectorType.Kind() == reflect.Struct && isSelectorPtr { - if rawValueType.ConvertibleTo(rawSelectorType) { - ptrValue := reflect.ValueOf(value) - if isValuePtr && ptrValue.IsNil() { + if rawDestType.Kind() == reflect.Struct && rawSrcType.Kind() == reflect.Struct && rawSrcType.ConvertibleTo(rawDestType) { + srcValue := reflect.ValueOf(value) + if srcIsPtr { + if srcValue.IsNil() { return nil, nil } - var destValue reflect.Value - if isValuePtr { - destValue = ptrValue.Elem().Convert(rawSelectorType) - } else { - destValue = ptrValue.Convert(rawSelectorType) - } - if isSelectorPtr { - destPtrType := reflect.New(valueType) - destPtrType.Elem().Set(destValue) - return destPtrType.Interface(), nil - } else { - return destValue.Interface(), nil - } + srcValue = srcValue.Elem() + } + converted := srcValue.Convert(rawDestType) + if destIsPtr { + out := reflect.New(rawDestType) + out.Elem().Set(converted) + return out.Interface(), nil } + return converted.Interface(), nil } if options.shallReportNotAssignable() { - //if !ensureAssignable(parameter.Name, selector.Type(), valueType) { - fmt.Printf("parameter %v is not directly assignable from %s:(%s)\nsrc:%s \ndst:%s\n", parameter.Name, parameter.In.Kind, parameter.In.Name, valueType.String(), selector.Type().String()) - //} + fmt.Printf("parameter %v is not directly assignable from %s:(%s)\nsrc:%s \ndst:%s\n", parameter.Name, parameter.In.Kind, parameter.In.Name, valueType.String(), destType.String()) } - reflectValue := reflect.New(valueType) //TODO replace with fast xreflect copy - valuePtr := reflectValue.Interface() + var target reflect.Value + if destIsPtr { + target = reflect.New(rawDestType) // *T where destType is *T + } else { + target = reflect.New(destType) // *T where destType is T + } if data, err := json.Marshal(value); err == nil { - if err = json.Unmarshal(data, valuePtr); err == nil { - value = reflectValue.Elem().Interface() + if err = json.Unmarshal(data, target.Interface()); err == nil { + if destIsPtr { + value = target.Interface() + } else { + value = target.Elem().Interface() + } } } } @@ -519,8 +640,8 @@ func (s *Session) lookupFirstValue(ctx context.Context, parameters []*state.Para } func (s *Session) LookupValue(ctx context.Context, parameter *state.Parameter, opts *Options) (value interface{}, has bool, err error) { - - if value, has, err = s.lookupValue(ctx, parameter, opts); err != nil { + value, has, err = s.lookupValue(ctx, parameter, opts) + if err != nil { err = response.NewParameterError("", parameter.Name, err, response.WithObject(value), response.WithErrorStatusCode(parameter.ErrorStatusCode)) } return value, has, err @@ -573,7 +694,8 @@ func (s *Session) lookupValue(ctx context.Context, parameter *state.Parameter, o if err != nil { return nil, false, fmt.Errorf("failed to locate parameter: %v, %w", parameter.Name, err) } - if value, has, err = parameterLocator.Value(ctx, parameter.In.Name); err != nil { + + if value, has, err = parameterLocator.Value(ctx, parameter.OutputType(), parameter.In.Name); err != nil { return nil, false, err } if parameter.In.Kind == state.KindConst && !has { //if parameter is const and has no value, use default value @@ -588,7 +710,7 @@ func (s *Session) lookupValue(ctx context.Context, parameter *state.Parameter, o if err != nil { return nil, false, fmt.Errorf("failed to locate parameter: %v, %w", baseParameter.Name, err) } - if value, has, err = parameterLocator.Value(ctx, baseParameter.In.Name); err != nil { + if value, has, err = parameterLocator.Value(ctx, baseParameter.OutputType(), baseParameter.In.Name); err != nil { return nil, false, err } } @@ -610,6 +732,13 @@ func (s *Session) adjustAndCache(ctx context.Context, parameter *state.Parameter return nil, false, err } if parameter.Output != nil { + // Defensive: ensure codec is initialized before Transform. + if !parameter.Output.Initialized() { + // Initialize using session resource and current parameter input type. + if initErr := parameter.Output.Init(s.resource, parameter.Schema.Type()); initErr != nil { + return nil, false, initErr + } + } transformed, err := parameter.Output.Transform(ctx, value, opts.codecOptions...) if err != nil { return nil, false, fmt.Errorf("failed to transform %s with %s: %v, %w", parameter.Name, parameter.Output.Name, value, err) @@ -700,7 +829,33 @@ func New(aView *view.View, opts ...Option) *Session { return ret } -func (s *Session) LoadState(parameters state.Parameters, aState interface{}) error { +type loadStateOptions struct { + skipKind map[state.Kind]bool + hasSkipKind bool + useHasMarker bool +} + +type LoadStateOption func(o *loadStateOptions) + +func WithHasMarker() LoadStateOption { + return func(o *loadStateOptions) { + o.useHasMarker = true + } +} +func WithLoadStateSkipKind(kinds ...state.Kind) LoadStateOption { + return func(o *loadStateOptions) { + for _, kind := range kinds { + o.skipKind[kind] = true + } + } +} + +func (s *Session) LoadState(parameters state.Parameters, aState interface{}, opts ...LoadStateOption) error { + options := &loadStateOptions{skipKind: map[state.Kind]bool{}} + for _, opt := range opts { + opt(options) + } + options.hasSkipKind = len(options.skipKind) > 0 rType := reflect.TypeOf(aState) sType := structology.NewStateType(rType, structology.WithCustomizedNames(func(name string, tag reflect.StructTag) []string { stateTag, _ := tags.ParseStateTags(tag, nil) @@ -711,29 +866,41 @@ func (s *Session) LoadState(parameters state.Parameters, aState interface{}) err })) inputState := sType.WithValue(aState) ptr := xunsafe.AsPointer(aState) + // Use presence markers only if enabled and supported by the input state + hasMarker := options.useHasMarker && inputState.HasMarker() for _, parameter := range parameters { if parameter.Scope != "" { continue } + + if options.hasSkipKind && options.skipKind[parameter.In.Kind] { + continue + } + + // Only warm cache for cacheable parameters; LookupValue only reads cache when cacheable + if !parameter.IsCacheable() { + continue + } selector, _ := inputState.Selector(parameter.Name) if selector == nil { continue } - if !selector.Has(ptr) { + // Only use selector.Has when input supports presence markers + if hasMarker && !selector.Has(ptr) { continue } value := selector.Value(ptr) switch parameter.In.Kind { case state.KindView, state.KindParam, state.KindState: if value == nil { - return nil + continue } rType := parameter.OutputType() if rType.Kind() == reflect.Ptr { ptr := (*unsafe.Pointer)(xunsafe.AsPointer(value)) if ptr == nil || *ptr == nil { - return nil + continue } } } @@ -746,7 +913,7 @@ func (s *Session) LoadState(parameters state.Parameters, aState interface{}) err func (s *Session) handleParameterError(parameter *state.Parameter, err error, errors *response.Errors) { if parameter.ErrorMessage != "" && err != nil { msg := strings.ReplaceAll(parameter.ErrorMessage, "${error}", err.Error()) - err = fmt.Errorf(msg) + err = fmt.Errorf("%s", msg) } if pErr, ok := err.(*response.Error); ok { pErr.Code = parameter.ErrorStatusCode diff --git a/service/session/state_test.go b/service/session/state_test.go new file mode 100644 index 000000000..497d6aca6 --- /dev/null +++ b/service/session/state_test.go @@ -0,0 +1,291 @@ +package session + +import ( + "reflect" + "testing" + + "github.com/viant/datly/view/state" + "github.com/viant/structology" +) + +func TestSessionEnsureValidValue_Transitions(t *testing.T) { + type T struct { + A *int + B *int + } + + inlineStructSwapped := reflect.StructOf([]reflect.StructField{ + // Deliberately swap field order vs T to ensure the types are not convertible. + {Name: "B", Type: reflect.TypeOf((*int)(nil))}, + {Name: "A", Type: reflect.TypeOf((*int)(nil))}, + }) + inlinePtrType := reflect.PtrTo(inlineStructSwapped) + + newSelector := func(t *testing.T, paramType reflect.Type) *structology.Selector { + t.Helper() + stateStruct := reflect.StructOf([]reflect.StructField{ + {Name: "Param", Type: paramType}, + }) + stateType := structology.NewStateType(stateStruct) + selector := stateType.Lookup("Param") + if selector == nil { + t.Fatalf("failed to lookup selector Param") + } + return selector + } + + intPtrType := reflect.TypeOf((*int)(nil)) + + ttPtrType := reflect.TypeOf((*T)(nil)) + sliceOfTTPtrType := reflect.SliceOf(ttPtrType) + ptrToSliceOfTTPtrType := reflect.PtrTo(sliceOfTTPtrType) + intType := reflect.TypeOf(int(0)) + sliceOfIntType := reflect.SliceOf(intType) + ttType := reflect.TypeOf(T{}) + + boolPtr := func(v bool) *bool { return &v } + + cases := []struct { + name string + schemaType reflect.Type + selectorType reflect.Type + required *bool + value interface{} + wantType reflect.Type + wantErr bool + check func(t *testing.T, got interface{}) + }{ + { + name: "nil-value_ptr-schema_returns-typed-nil", + schemaType: intPtrType, + selectorType: intPtrType, + value: nil, + wantType: intPtrType, + check: func(t *testing.T, got interface{}) { + t.Helper() + if !reflect.ValueOf(got).IsNil() { + t.Fatalf("expected nil pointer, got %v", got) + } + }, + }, + { + name: "nil-value_slice-schema_returns-nil-slice", + schemaType: sliceOfIntType, + selectorType: sliceOfIntType, + value: nil, + wantType: sliceOfIntType, + check: func(t *testing.T, got interface{}) { + t.Helper() + if !reflect.ValueOf(got).IsNil() { + t.Fatalf("expected nil slice, got %v", got) + } + }, + }, + { + name: "ptr-struct_to_ptr-to-slice-wraps-single", + schemaType: sliceOfTTPtrType, + selectorType: ptrToSliceOfTTPtrType, + value: func() interface{} { + a := 10 + b := 20 + return &T{A: &a, B: &b} + }(), + wantType: ptrToSliceOfTTPtrType, + check: func(t *testing.T, got interface{}) { + t.Helper() + gotSlicePtr := reflect.ValueOf(got) + if gotSlicePtr.IsNil() { + t.Fatalf("expected non-nil pointer to slice") + } + gotSlice := gotSlicePtr.Elem() + if gotSlice.Len() != 1 { + t.Fatalf("expected len=1, got %d", gotSlice.Len()) + } + if gotSlice.Index(0).IsNil() { + t.Fatalf("expected element 0 to be non-nil") + } + }, + }, + { + name: "ptr-struct-nil_to_ptr-to-slice-wraps-empty", + schemaType: sliceOfTTPtrType, + selectorType: ptrToSliceOfTTPtrType, + value: (*T)(nil), + wantType: ptrToSliceOfTTPtrType, + check: func(t *testing.T, got interface{}) { + t.Helper() + gotSlicePtr := reflect.ValueOf(got) + if gotSlicePtr.IsNil() { + t.Fatalf("expected non-nil pointer to slice") + } + gotSlice := gotSlicePtr.Elem() + if gotSlice.Len() != 0 { + t.Fatalf("expected len=0, got %d", gotSlice.Len()) + } + }, + }, + { + name: "slice-to-scalar_len0_required_errors", + schemaType: ttPtrType, + selectorType: ttPtrType, + required: boolPtr(true), + value: []*T{}, + wantErr: true, + }, + { + name: "slice-to-scalar_len0_not-required_returns-zero", + schemaType: ttPtrType, + selectorType: ttPtrType, + value: []*T{}, + wantType: ttPtrType, + check: func(t *testing.T, got interface{}) { + t.Helper() + if reflect.ValueOf(got).IsNil() { + t.Fatalf("expected non-nil *T") + } + }, + }, + { + name: "slice-of-int_len1_to-int", + schemaType: intType, + selectorType: intType, + value: []int{7}, + wantType: intType, + check: func(t *testing.T, got interface{}) { + t.Helper() + if got.(int) != 7 { + t.Fatalf("expected 7, got %v", got) + } + }, + }, + { + name: "slice-of-int_len2_to-int_errors", + schemaType: intType, + selectorType: intType, + value: []int{1, 2}, + wantErr: true, + }, + { + name: "ptr-required_nil_errors", + schemaType: ttPtrType, + selectorType: ttPtrType, + required: boolPtr(true), + value: (*T)(nil), + wantErr: true, + }, + { + name: "ptr-value_to-struct-selector_derefs", + schemaType: ttType, + selectorType: ttType, + value: func() interface{} { + a := 3 + b := 4 + return &T{A: &a, B: &b} + }(), + wantType: ttType, + check: func(t *testing.T, got interface{}) { + t.Helper() + gotT := got.(T) + if gotT.A == nil || gotT.B == nil { + t.Fatalf("expected non-nil fields") + } + if *gotT.A != 3 || *gotT.B != 4 { + t.Fatalf("unexpected values: %+v", gotT) + } + }, + }, + { + name: "struct-value_to-ptr-selector_allocates", + schemaType: ttPtrType, + selectorType: ttPtrType, + value: func() interface{} { + a := 5 + b := 6 + return T{A: &a, B: &b} + }(), + wantType: ttPtrType, + check: func(t *testing.T, got interface{}) { + t.Helper() + gotPtr := got.(*T) + if gotPtr == nil || gotPtr.A == nil || gotPtr.B == nil { + t.Fatalf("expected non-nil *T with non-nil fields") + } + if *gotPtr.A != 5 || *gotPtr.B != 6 { + t.Fatalf("unexpected values: %+v", *gotPtr) + } + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + parameter := &state.Parameter{ + Name: "Param", + In: state.NewState("Param"), + Schema: state.NewSchema(tc.schemaType), + Required: tc.required, + } + + selector := newSelector(t, tc.selectorType) + sess := &Session{} + opts := NewOptions(WithReportNotAssignable(false)) + + got, err := sess.ensureValidValue(tc.value, parameter, selector, opts) + if (err != nil) != tc.wantErr { + t.Fatalf("error=%v, wantErr=%v", err, tc.wantErr) + } + if tc.wantErr { + return + } + if tc.wantType != nil && reflect.TypeOf(got) != tc.wantType { + t.Fatalf("expected %v, got %T", tc.wantType, got) + } + if tc.check != nil { + tc.check(t, got) + } + }) + } + + t.Run("slice-of-named-ptr_to-inline-ptr_allocates-and-copies_details", func(t *testing.T) { + a := 1 + b := 2 + original := &T{A: &a, B: &b} + input := []*T{original} + + parameter := &state.Parameter{ + Name: "Param", + In: state.NewState("Param"), + Schema: state.NewSchema(inlinePtrType), + } + selector := newSelector(t, inlinePtrType) + sess := &Session{} + opts := NewOptions(WithReportNotAssignable(false)) + + got, err := sess.ensureValidValue(input, parameter, selector, opts) + if err != nil { + t.Fatalf("ensureValidValue error: %v", err) + } + if reflect.TypeOf(got) != inlinePtrType { + t.Fatalf("expected %v, got %T", inlinePtrType, got) + } + + gotPtr := reflect.ValueOf(got).Pointer() + origPtr := reflect.ValueOf(original).Pointer() + if gotPtr == origPtr { + t.Fatalf("expected ensureValidValue to allocate/copy into %v; got aliases original *T pointer %x", inlinePtrType, gotPtr) + } + + gotValue := reflect.ValueOf(got).Elem() + gotA := gotValue.FieldByName("A") + gotB := gotValue.FieldByName("B") + if gotA.IsNil() || gotB.IsNil() { + t.Fatalf("expected A and B to be non-nil") + } + if gotA.Elem().Int() != int64(*original.A) { + t.Fatalf("expected A=%d, got %d", *original.A, gotA.Elem().Int()) + } + if gotB.Elem().Int() != int64(*original.B) { + t.Fatalf("expected B=%d, got %d", *original.B, gotB.Elem().Int()) + } + }) +} diff --git a/service/session/stater.go b/service/session/stater.go index 2c75bcbe9..332c0206c 100644 --- a/service/session/stater.go +++ b/service/session/stater.go @@ -2,11 +2,19 @@ package session import ( "context" + "fmt" + "net/http" + "reflect" + "runtime/debug" + + "embed" + "github.com/viant/datly/utils/types" + "github.com/viant/datly/view" "github.com/viant/datly/view/state" "github.com/viant/datly/view/state/kind/locator" + "github.com/viant/xdatly/handler/response" hstate "github.com/viant/xdatly/handler/state" - "reflect" ) func (s *Session) ValuesOf(ctx context.Context, any interface{}) (map[string]interface{}, error) { @@ -35,15 +43,48 @@ func (s *Session) Into(ctx context.Context, dest interface{}, opts ...hstate.Opt } func (s *Session) Bind(ctx context.Context, dest interface{}, opts ...hstate.Option) (err error) { + defer func() { + if r := recover(); r != nil { + panicMsg := fmt.Sprintf("Panic occurred: %v, Stack trace: %v", r, string(debug.Stack())) + logger := s.Logger() + if logger == nil { + panic(panicMsg) + } + s.Logger().Errorc(ctx, panicMsg) + err = response.NewError(http.StatusInternalServerError, "Internal server error") + } + }() + destType := reflect.TypeOf(dest) sType := types.EnsureStruct(destType) stateType, ok := s.Types.Lookup(sType) - if !ok { - if stateType, err = state.NewType( - state.WithSchema(state.NewSchema(destType)), - state.WithResource(s.resource), - ); err != nil { - return err + + var embedFs *embed.FS + if embedder, ok := dest.(state.Embedder); ok { + embedFs = embedder.EmbedFS() + } + + if !ok && s.component != nil { + + if s.component.Input.Type.Type() != nil { + if destType == s.component.Input.Type.Type().Type() { + stateType = &s.component.Input.Type + } + } + if s.component.Output.Type.Type() != nil { + if destType == s.component.Output.Type.Type().Type() { + stateType = &s.component.Output.Type + } + } + + if stateType == nil { + if stateType, err = state.NewType( + state.WithSchema(state.NewSchema(destType)), + state.WithResource(s.resource), + state.WithFS(embedFs), + ); err != nil { + return err + } } s.Types.Put(stateType) } @@ -52,9 +93,11 @@ func (s *Session) Bind(ctx context.Context, dest interface{}, opts ...hstate.Opt } hOptions := hstate.NewOptions(opts...) - aState := stateType.Type().WithValue(dest) - var stateOptions []locator.Option + aState := stateType.Type().WithValue(dest) + var stateOptions = []locator.Option{ + locator.WithLogger(s.logger), + } var locatorsToRemove = []state.Kind{state.KindComponent} if hOptions.Constants() != nil { stateOptions = append(stateOptions, locator.WithConstants(hOptions.Constants())) @@ -93,33 +136,107 @@ func (s *Session) Bind(ctx context.Context, dest interface{}, opts ...hstate.Opt stateOptions = append(viewOptions.kindLocator.Options(), stateOptions...) } - if s.component != nil && s.component.Contract.Output.Type.Type().Type() == destType { - return s.handleComponentpOutputType(ctx, dest, stateOptions) + if err = s.handleInputState(ctx, hOptions, embedFs); err != nil { + return err + } + + if s.component != nil { + componentOutputType := types.EnsureStruct(s.component.Contract.Output.Type.Type().Type()) + if componentOutputType == types.EnsureStruct(destType) { + return s.handleComponentOutputType(ctx, dest, stateOptions) + } } options := s.Indirect(true, stateOptions...) options.scope = hOptions.Scope() + if err = s.SetState(ctx, stateType.Parameters, aState, options); err != nil { return err } + if initializer, ok := dest.(state.Initializer); ok { err = initializer.Init(ctx) } return err } -func (s *Session) handleComponentpOutputType(ctx context.Context, dest interface{}, stateOptions []locator.Option) error { +func (s *Session) handleInputState(ctx context.Context, hOptions *hstate.Options, embedFs *embed.FS) error { + // Handle WithInput: preload cache from provided input data + input := hOptions.Input() + if input == nil { + return nil + } + var parameters state.Parameters + var inputType *state.Type + // If input type matches component input type, reuse component parameters + if s.component != nil && s.component.Input.Type.Type() != nil && s.component.Input.Type.Type().Type() != nil { + compInType := s.component.Input.Type.Type().Type() + inType := reflect.TypeOf(input) + if inType != nil && compInType != nil && types.EnsureStruct(inType) == types.EnsureStruct(compInType) { + parameters = s.component.Input.Type.Parameters + inputType = &s.component.Input.Type + } + } + // Otherwise, derive parameters from input type + if len(parameters) == 0 { + inType := reflect.TypeOf(input) + aType, e := state.NewType( + state.WithFS(embedFs), + state.WithSchema(state.NewSchema(inType)), + state.WithResource(s.resource), + ) + if e != nil { + return e + } + if e = aType.Init(); e != nil { + return e + } + inputType = aType + for _, p := range aType.Parameters { + p.Init(ctx, s.view.Resource()) + } + parameters = aType.Parameters + } + + var skipOption []LoadStateOption + skipOption = append(skipOption, WithHasMarker()) + if s.view.Mode != view.ModeQuery { + //this is for patch component only (in the future we may pass it to caller when call Bind + skipOption = append(skipOption, WithLoadStateSkipKind(state.KindView, state.KindParam)) + } + if e := s.LoadState(parameters, input, skipOption...); e != nil { + return e + } + if s.view.Mode == view.ModeQuery { + inputState := inputType.Type().WithValue(input) + options := s.Options.Indirect(true) + if err := s.SetState(ctx, parameters, inputState, options); err != nil { + return err + } + _ = s.SetViewState(ctx, s.view) + } + return nil +} + +func (s *Session) handleComponentOutputType(ctx context.Context, dest interface{}, stateOptions []locator.Option) error { sessionOpt := s.Options s.Options = *s.Indirect(true, stateOptions...) destValue, err := s.operate(ctx, s, s.component) - s.Options = sessionOpt - - if destValue != nil { - reflect.ValueOf(dest).Elem().Set(reflect.ValueOf(destValue).Elem()) + destPtr := reflect.ValueOf(dest) + if err != nil && destValue == nil { + if errorSetter, ok := dest.(response.StatusSetter); ok { + errorSetter.SetError(err) + return nil + } + return err } + s.Options = sessionOpt + reflectDestValue := reflect.ValueOf(destValue) - if err != nil { - return err + if reflectDestValue.Kind() == reflect.Ptr { + destPtr.Elem().Set(reflectDestValue.Elem()) + } else { + destPtr.Elem().Set(reflectDestValue) } return nil } diff --git a/shared/http.go b/shared/http.go index 5b84f39f5..6be311b52 100644 --- a/shared/http.go +++ b/shared/http.go @@ -3,22 +3,56 @@ package shared import ( "bytes" "io" + "mime" "net/http" + "strings" ) // CloneHTTPRequest clones http request func CloneHTTPRequest(request *http.Request) (*http.Request, error) { - var data []byte - var err error + // Shallow clone; special-case multipart to avoid buffering entire body ret := *request ret.URL = request.URL - if request.Body != nil { - if data, err = readRequestBody(request); err != nil { - return nil, err + + if request.Body == nil { + return &ret, nil + } + + // Detect multipart/*; avoid reading/consuming body + if IsMultipartRequest(request) { + // If multipart form has already been parsed, we don't need to + // share or re-read the body. Instead, reuse the parsed form and + // multipart data on the clone so that downstream logic can access + // form values without touching the body again. + if request.MultipartForm != nil { + // Body is no longer needed for form access. + ret.Body = http.NoBody + // Reuse parsed forms and multipart metadata. + ret.MultipartForm = request.MultipartForm + if request.Form != nil { + ret.Form = request.Form + } + if request.PostForm != nil { + ret.PostForm = request.PostForm + } + + return &ret, nil } - ret.Body = io.NopCloser(bytes.NewReader(data)) + + // Backwards compatibility: if the multipart form hasn't been + // parsed yet, fall back to sharing the body. Callers must + // still ensure only one reader consumes it. + ret.Body = request.Body + return &ret, nil } - return &ret, err + + // Non-multipart: safe full read, reset both original and clone bodies + data, err := readRequestBody(request) + if err != nil { + return nil, err + } + ret.Body = io.NopCloser(bytes.NewReader(data)) + return &ret, nil } func readRequestBody(request *http.Request) ([]byte, error) { @@ -30,3 +64,28 @@ func readRequestBody(request *http.Request) ([]byte, error) { request.Body = io.NopCloser(bytes.NewReader(data)) return data, err } + +// IsMultipartRequest returns true if request Content-Type is multipart/* +func IsMultipartRequest(r *http.Request) bool { + if r == nil || r.Header == nil { + return false + } + return IsMultipartContentType(r.Header.Get("Content-Type")) +} + +// IsMultipartContentType returns true when the Content-Type header indicates any multipart/* +func IsMultipartContentType(ct string) bool { + if ct == "" { + return false + } + mediaType, _, err := mime.ParseMediaType(ct) + if err != nil { + return strings.Contains(strings.ToLower(ct), "multipart/") + } + return strings.HasPrefix(strings.ToLower(mediaType), "multipart/") +} + +// IsFormData returns true when mediaType equals multipart/form-data +func IsFormData(mediaType string) bool { + return strings.EqualFold(mediaType, "multipart/form-data") +} diff --git a/shared/logging/logger.go b/shared/logging/logger.go new file mode 100644 index 000000000..de26bc87d --- /dev/null +++ b/shared/logging/logger.go @@ -0,0 +1,441 @@ +package logging + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + regexp "regexp" + "runtime" + strings "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/viant/xdatly/handler/exec" + "github.com/viant/xdatly/handler/logger" +) + +const ( + ReqId = "RequestId" + OpenTelemetryTraceId = "OpenTelemetryTraceId" + DEBUG = "DEBUG" + INFO = "INFO" + WARN = "WARN" + ERROR = "ERROR" + UNKNOWN = "UNKNOWN" // Indicate other environment + DefaultTraceIdKey = "reqTraceId" +) + +type slogger struct { + logger *slog.Logger + level slog.Level + traceIdKey string +} + +type Option func(l *slogger) + +func WithTraceIdKey(key string) Option { + return func(l *slogger) { + l.traceIdKey = key + } +} + +// Init creates an ISLogger instance, a structured logger using the JSON Handler. +// Creating this logger sets this as the default logger, so any logging after this +// which goes through the standard logging package will also produce JSON structured +// logs. +func New(level string, dest io.Writer, opts ...Option) logger.Logger { + if dest == nil { + dest = os.Stdout + } + + logLevel := slog.LevelInfo + switch strings.ToUpper(level) { + case DEBUG: + logLevel = slog.LevelDebug + case WARN: + logLevel = slog.LevelWarn + case ERROR: + logLevel = slog.LevelError + } + + handler := slog.NewJSONHandler(dest, &slog.HandlerOptions{ + AddSource: false, + Level: logLevel, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + // Rename the time key to "timestamp" + if a.Key == slog.TimeKey { + a.Key = "timestamp" + } + return a + }, + }) + sl := slog.New(handler) + slog.SetDefault(sl) + l := &slogger{sl, logLevel, DefaultTraceIdKey} + for _, opt := range opts { + opt(l) + } + + return l +} + +func (s *slogger) IsDebugEnabled() bool { + return s.level.Level() <= slog.LevelDebug +} + +func (s *slogger) IsInfoEnabled() bool { + return s.level.Level() <= slog.LevelInfo +} + +func (s *slogger) IsWarnEnabled() bool { + return s.level.Level() <= slog.LevelWarn +} + +func (s *slogger) IsErrorEnabled() bool { + return s.level.Level() <= slog.LevelError +} + +// getCallerInfo uses runtime to get the caller's program counter +// and extract info from the stack frame to get the function name, etc. +func (s *slogger) getCallerInfo() []any { + callers := make([]uintptr, 1) + count := runtime.Callers(3, callers[:]) // skip to actual caller + if count == 0 { + slog.Warn("getCallerInfo: no frames, exiting") + return nil + } + + frames := runtime.CallersFrames(callers) + var frame runtime.Frame + var more bool + for { + frame, more = frames.Next() + if !more { + break + } + } + + attr := []any{ + "function", frame.Function, "file", frame.File, "line", frame.Line, + } + + return attr +} + +// getContextValues retrieves "known" logging values from the Context. +// These values can be added to the Context using the provided utility functions. +func (s *slogger) getContextValues(ctx context.Context) []any { + var values []any + if ctx == nil { + slog.Warn("getContextValues: ctx is nil") + return nil + } + + openTelemetryTraceId := ctx.Value(OpenTelemetryTraceId) + if openTelemetryTraceId != nil { + values = append(values, "OpenTelemetryTraceId", openTelemetryTraceId) + } + + execContext := exec.GetContext(ctx) + if execContext != nil { + traceId := "unknown" + + // ideally TraceID and Trace.TraceID should be the same + // but xdatly/handler/exec.(*Context).setHeader sets TraceID first + // with the value of XDATLY_TRACING_HEADER env var value header (adp-request-id for datly platform) + if execContext.TraceID != "" { + traceId = execContext.TraceID + } else if execContext.Trace != nil { + traceId = execContext.Trace.TraceID + } + values = append(values, s.traceIdKey, traceId) + } + + return values +} + +// Info wraps a call to slog.Info, inserting details for the calling function. +func (s *slogger) Info(msg string, args ...any) { + if !s.IsInfoEnabled() { + return + } + caller := s.getCallerInfo() + caller = append(caller, args...) + s.logger.Info(msg, caller...) +} + +// Debug wraps a call to slog.Debug, inserting details for the calling function. +func (s *slogger) Debug(msg string, args ...any) { + if !s.IsDebugEnabled() { + return + } + caller := s.getCallerInfo() + caller = append(caller, args...) + s.logger.Debug(msg, caller...) +} + +// Warn wraps a call to slog.Warn, inserting details for the calling function. +func (s *slogger) Warn(msg string, args ...any) { + if !s.IsWarnEnabled() { + return + } + caller := s.getCallerInfo() + caller = append(caller, args...) + s.logger.Warn(msg, caller...) +} + +// Error wraps a call to slog.Error, inserting details for the calling function. +func (s *slogger) Error(msg string, args ...any) { + if !s.IsErrorEnabled() { + return + } + caller := s.getCallerInfo() + caller = append(caller, args...) + s.logger.Error(msg, caller...) +} + +// Infoc wraps a call to slog.Info, inserting details for the calling function, +// and retrieving known values from the context object. +func (s *slogger) Infoc(ctx context.Context, msg string, args ...any) { + if !s.IsInfoEnabled() { + return + } + caller := s.getCallerInfo() + values := s.getContextValues(ctx) + caller = append(caller, values...) + caller = append(caller, args...) + s.logger.Info(msg, caller...) +} + +func (s *slogger) Infos(ctx context.Context, msg string, attrs ...slog.Attr) { + if !s.IsInfoEnabled() { + return + } + caller := s.getCallerInfo() + values := s.getContextValues(ctx) + caller = append(caller, values...) + caller = append(caller, redactAttrs(attrs...)...) + + s.logger.Info(msg, caller...) +} + +// Debugc wraps a call to slog.Debug, inserting details for the calling function, +// and retrieving known values from the context object. +func (s *slogger) Debugc(ctx context.Context, msg string, args ...any) { + if !s.IsDebugEnabled() { + return + } + caller := s.getCallerInfo() + values := s.getContextValues(ctx) + caller = append(caller, values...) + caller = append(caller, args...) + s.logger.Debug(msg, caller...) +} + +func (s *slogger) Debugs(ctx context.Context, msg string, attrs ...slog.Attr) { + if !s.IsDebugEnabled() { + return + } + caller := s.getCallerInfo() + values := s.getContextValues(ctx) + caller = append(caller, values...) + caller = append(caller, redactAttrs(attrs...)...) + + s.logger.Debug(msg, caller...) +} + +// DebugJSONc wraps a call to slog.Debug, inserting details for the calling function, +// and retrieving known values from the context object. +func (s *slogger) DebugJSONc(ctx context.Context, msg string, obj any) { + caller := s.getCallerInfo() + values := s.getContextValues(ctx) + caller = append(caller, values...) + + // Initialize request and jsonData variables + var request events.APIGatewayProxyRequest + var jsonData []byte + // Marshal the object to JSON string + jsonString, _ := json.Marshal(obj) + // Unmarshal JSON string to APIGatewayProxyRequest + err := json.Unmarshal(jsonString, &request) + if err != nil { + return + } + + // Check if the request has an HTTP method + if len(request.HTTPMethod) > 0 { + if request.MultiValueHeaders == nil { + request.MultiValueHeaders = make(map[string][]string) + } + // Remove Authorization header + request.MultiValueHeaders["Authorization"] = nil + request.Headers["Authorization"] = "" + // Marshal the modified request to JSON + jsonData, _ = json.Marshal(request) + } else { + jsonData = jsonString + } + msg = fmt.Sprintf("%s %s", msg, string(jsonData)) + s.Debugc(ctx, msg, caller...) +} + +// Warnc wraps a call to slog.Warn, inserting details for the calling function, +// and retrieving known values from the context object. +func (s *slogger) Warnc(ctx context.Context, msg string, args ...any) { + if !s.IsWarnEnabled() { + return + } + caller := s.getCallerInfo() + values := s.getContextValues(ctx) + caller = append(caller, values...) + caller = append(caller, args...) + s.logger.Warn(msg, caller...) +} + +func (s *slogger) Warns(ctx context.Context, msg string, attrs ...slog.Attr) { + if !s.IsWarnEnabled() { + return + } + caller := s.getCallerInfo() + values := s.getContextValues(ctx) + caller = append(caller, values...) + caller = append(caller, redactAttrs(attrs...)...) + + s.logger.Warn(msg, caller...) +} + +// Errorc wraps a call to slog.Error, inserting details for the calling function, +// and retrieving known values from the context object. +func (s *slogger) Errorc(ctx context.Context, msg string, args ...any) { + if !s.IsErrorEnabled() { + return + } + caller := s.getCallerInfo() + values := s.getContextValues(ctx) + caller = append(caller, values...) + caller = append(caller, args...) + s.logger.Error(msg, caller...) +} + +func (s *slogger) Errors(ctx context.Context, msg string, attrs ...slog.Attr) { + if !s.IsErrorEnabled() { + return + } + caller := s.getCallerInfo() + values := s.getContextValues(ctx) + caller = append(caller, values...) + caller = append(caller, redactAttrs(attrs...)...) + + s.logger.Error(msg, caller...) +} + +// Helper to get platform from environment suffix +func getPlatformFromEnv(environment string) string { + switch { + case strings.Contains(environment, "dev"): + return "development" + case strings.Contains(environment, "stage"): + return "stage" + case strings.Contains(environment, "prod"): + return "production" + default: + return UNKNOWN + } +} + +// redactAttrs applies redaction rules to slog.Attr list. +// Skip redactValue for primitive types to avoid unnecessary processing +// This avoids redundant type switch/marshalling cost in high-volume logging +func redactAttrs(attrs ...slog.Attr) []any { + var result []any + for _, attr := range attrs { + if isSensitiveKey(attr.Key) { + result = append(result, slog.String(attr.Key, "[REDACTED]")) + continue + } + val := attr.Value.Any() + switch val.(type) { + case int, int64, float64, bool, nil: + result = append(result, attr) + default: + redactedValue := slog.AnyValue(redactValue(val)) + result = append(result, slog.Attr{Key: attr.Key, Value: redactedValue}) + } + } + return result +} + +// redactValue recursively redacts sensitive info in maps, slices, or structs. +func redactValue(value any) any { + switch v := value.(type) { + case string: + // Redact sensitive information in strings + return redactSensitiveInfo(v) + case int, int64, float64, bool, nil: + // Return primitive values directly (skip JSON marshalling) + return v + case map[string]any: + // Redact value if key is sensitive (e.g., Authorization → [REDACTED]) + // Ensures map fields are redacted even if value doesn’t match regex + for key, val := range v { + if isSensitiveKey(key) { + v[key] = "[REDACTED]" + } else { + v[key] = redactValue(val) + } + } + return v + case []any: + // Recursively redact sensitive information in slices + for i, val := range v { + v[i] = redactValue(val) + } + return v + default: + // Only marshal/unmarshal if absolutely needed (structs, unknown). + jsonData, err := json.Marshal(v) // Converts struct to map to enable nested field redaction. + if err != nil { + return v // If marshal fails, skip redaction + } + var unmarshaled any + if err := json.Unmarshal(jsonData, &unmarshaled); err != nil { + return v // If unmarshal fails, skip redaction + } + return redactValue(unmarshaled) + } +} + +// redactSensitiveInfo redacts known patterns in a string (e.g., tokens in URLs). +func redactSensitiveInfo(value string) string { + sensitivePatterns := []*regexp.Regexp{ + // Redact key=value style + regexp.MustCompile(`(?i)(X-Amz-Security-Token|X-Amz-Signature|X-Amz-Credential|Authorization|password|token|apiKey)=([^&\s]+)`), + // Redact key: value or key value + regexp.MustCompile(`(?i)(Authorization|password|token|apiKey)[\s:=]+([^&\s]+)`), + // Redact URL with user:pass@host + regexp.MustCompile(`(?i)https?://[^/]+:[^@]+@`), + } + + redacted := value + for _, pattern := range sensitivePatterns { + redacted = pattern.ReplaceAllString(redacted, "$1=[REDACTED]") + } + return redacted +} + +// isSensitiveKey returns true if the key is known to contain sensitive data. +func isSensitiveKey(key string) bool { + sensitiveKeys := []string{ + "authorization", "token", "apikey", "password", + "credential", "secret", "access_key", "secret_key", + } + key = strings.ToLower(key) + for _, sk := range sensitiveKeys { + if key == sk { + return true + } + } + return false +} diff --git a/utils/errors/db.go b/utils/errors/db.go new file mode 100644 index 000000000..19067f0ce --- /dev/null +++ b/utils/errors/db.go @@ -0,0 +1,50 @@ +package errors + +import ( + "errors" + "strings" +) + +// IsDatabaseError determines whether the supplied error was caused by the database or driver layer. +// We inspect the full error chain because many call-sites wrap driver errors with additional context. +func IsDatabaseError(err error) bool { + if err == nil { + return false + } + return hasDatabaseSignature(err) +} + +func hasDatabaseSignature(err error) bool { + for err != nil { + if matchesDatabasePattern(err.Error()) { + return true + } + err = errors.Unwrap(err) + } + return false +} + +func matchesDatabasePattern(message string) bool { + if message == "" { + return false + } + lower := strings.ToLower(message) + patterns := []string{ + "database error occured while fetching data", + "database error occurred while fetching data", + "error occured while connecting to database", + "error occurred while connecting to database", + "failed to get db", + "failed to create stmt source", + "too many connections", + "connection refused", + "driver: bad connection", + "sql: transaction has already been committed or rolled back", + } + for _, pattern := range patterns { + if strings.Contains(lower, pattern) { + return true + } + } + return false +} diff --git a/view/config.go b/view/config.go index 485fe5f45..5dd70f6de 100644 --- a/view/config.go +++ b/view/config.go @@ -3,12 +3,13 @@ package view import ( "context" "fmt" + "reflect" + "strings" + "github.com/viant/datly/shared" "github.com/viant/datly/view/state" "github.com/viant/xdatly/codec" "github.com/viant/xreflect" - "reflect" - "strings" ) const ( @@ -90,6 +91,11 @@ func (c *Config) GetContentFormatParameter() *state.Parameter { return QueryStateParameters.ContentFormatParameter } +func (c *Constraints) HasOrderByColumn(name string) bool { + _, ok := c.OrderByColumn[name] + return ok +} + func (c *Config) Init(ctx context.Context, resource *Resource, parent *View) error { if err := c.ensureConstraints(resource); err != nil { return err diff --git a/view/extension/handler/loader.go b/view/extension/handler/loader.go index 7dd4fc96f..86a5966c2 100644 --- a/view/extension/handler/loader.go +++ b/view/extension/handler/loader.go @@ -6,6 +6,7 @@ import ( "compress/gzip" "context" "encoding/json" + "errors" "fmt" "github.com/viant/afs" "github.com/viant/datly/utils/types" @@ -42,56 +43,92 @@ func (l *LoadData) Exec(ctx context.Context, session handler.Session) (interface if !ok || err != nil { return nil, fmt.Errorf("invalid Loader URL: %w", err) } + var URL string - switch URLValue.(type) { + switch v := URLValue.(type) { case string: - URL = URLValue.(string) + URL = v case *string: - URL = *URLValue.(*string) + URL = *v default: - return nil, fmt.Errorf("invalid Loader URL: expected %T, but had %T", URL, URLValue) + return nil, fmt.Errorf("invalid Loader URL: expected %T, but had %T", "", URLValue) } + // Prefer .gz if the plain URL doesn't exist. if ok, _ := l.fs.Exists(ctx, URL); !ok { if ok, _ := l.fs.Exists(ctx, URL+".gz"); ok { URL += ".gz" } } - isCompressed := strings.HasSuffix(URL, ".gz") + // Download compressed or plain bytes (API returns []byte). data, err := l.fs.DownloadWithURL(ctx, URL) if err != nil { return nil, fmt.Errorf("failed to load URL: %w", err) } - if isCompressed { - reader, err := gzip.NewReader(bytes.NewReader(data)) + + // Build a streaming reader chain; avoid io.ReadAll on gzip. + var r io.Reader = bytes.NewReader(data) + if strings.HasSuffix(URL, ".gz") { + gzr, err := gzip.NewReader(r) if err != nil { return nil, fmt.Errorf("failed to decompress URL: failed to create reader: %w (used URL: %s)", err, URL) } - defer reader.Close() - if data, err = io.ReadAll(reader); err != nil { - return nil, fmt.Errorf("failed to decompress URL:%w (used URL: %s)", err, URL) - } + defer gzr.Close() + r = gzr } + + br := bufio.NewReaderSize(r, 1<<20) // read-ahead; does NOT cap JSON size + dec := json.NewDecoder(br) + dec.UseNumber() + + // Output slice + appender (kept from your original design) itemType := l.Options.OutputType.Elem() xSlice := xunsafe.NewSlice(l.Options.OutputType) - scanner := bufio.NewScanner(bytes.NewReader(data)) response := reflect.New(l.Options.OutputType).Interface() appender := xSlice.Appender(xunsafe.AsPointer(response)) - scanner.Buffer(make([]byte, 1024*1024), 5*1024*1024) - for scanner.Scan() { - line := scanner.Bytes() - if len(line) == 0 { - continue + + // Reject top-level arrays to keep the code simple (no streaming array parsing). + first, err := peekFirstNonSpace(br) + if err != nil { + if errors.Is(err, io.EOF) { + return response, nil // empty file -> empty slice } - item := types.NewValue(itemType) - err := json.Unmarshal(scanner.Bytes(), item) - if err != nil { - return nil, fmt.Errorf("invalid item: %w, %s", err, line) + return nil, fmt.Errorf("read error: %w", err) + } + if first == '[' { + return nil, fmt.Errorf("top-level JSON arrays are not supported; provide NDJSON (one object per line) or a single JSON object") + } + // Put the byte back so the decoder sees it. + _ = br.UnreadByte() + + // Decode one value per call: supports single object or NDJSON. + for { + item := types.NewValue(itemType) // pointer to zero value of element type + if err := dec.Decode(item); err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, fmt.Errorf("invalid item: %w", err) } appender.Append(item) } - return response, scanner.Err() + + return response, nil +} + +// Reads and returns the first non-space byte without consuming input for the decoder. +func peekFirstNonSpace(br *bufio.Reader) (byte, error) { + for { + b, err := br.ReadByte() + if err != nil { + return 0, err + } + if b == ' ' || b == '\n' || b == '\r' || b == '\t' { + continue + } + return b, nil + } } func (*LoadDataProvider) New(ctx context.Context, opts ...handler.Option) (handler.Handler, error) { diff --git a/view/extension/init.go b/view/extension/init.go index db515af42..ea5ff3730 100644 --- a/view/extension/init.go +++ b/view/extension/init.go @@ -3,6 +3,9 @@ package extension import ( "encoding/json" "fmt" + "mime/multipart" + "net/http" + dcodec "github.com/viant/datly/view/extension/codec" "github.com/viant/datly/view/extension/handler" "github.com/viant/datly/view/extension/marshaller" @@ -17,14 +20,14 @@ import ( "github.com/viant/xdatly/handler/response/tabular/tjson" "github.com/viant/xdatly/handler/response/tabular/xml" "github.com/viant/xdatly/handler/validator" - "net/http" + + "reflect" + "time" "github.com/viant/xdatly/predicate" "github.com/viant/xdatly/types/core" _ "github.com/viant/xdatly/types/custom" "github.com/viant/xreflect" - "reflect" - "time" ) const ( @@ -50,7 +53,8 @@ func InitRegistry() { xreflect.NewType("validator.Violation", xreflect.WithReflectType(reflect.TypeOf(validator.Violation{}))), xreflect.NewType("RawMessage", xreflect.WithReflectType(reflect.TypeOf(json.RawMessage{}))), xreflect.NewType("json.RawMessage", xreflect.WithReflectType(reflect.TypeOf(json.RawMessage{}))), - xreflect.NewType("json.RawMessage", xreflect.WithReflectType(reflect.TypeOf(json.RawMessage{}))), + xreflect.NewType("FileHeader", xreflect.WithReflectType(reflect.TypeOf(multipart.FileHeader{}))), + xreflect.NewType("multipart.FileHeader", xreflect.WithReflectType(reflect.TypeOf(multipart.FileHeader{}))), xreflect.NewType("types.BitBool", xreflect.WithReflectType(reflect.TypeOf(types.BitBool(true)))), xreflect.NewType("time.Time", xreflect.WithReflectType(xreflect.TimeType)), xreflect.NewType("response.Status", xreflect.WithReflectType(reflect.TypeOf(response.Status{}))), @@ -119,6 +123,7 @@ func InitRegistry() { PredicateGreaterOrEqual: NewGreaterOrEqualPredicate(), PredicateGreaterThan: NewGreaterThanPredicate(), PredicateLike: NewLikePredicate(), + PredicateExpr: NewExprPredicate(), PredicateNotLike: NewNotLikePredicate(), PredicateHandler: NewPredicateHandler(), PredicateContains: NewContainsPredicate(), diff --git a/view/extension/predicates.go b/view/extension/predicates.go index 04b0ba098..ae0276e95 100644 --- a/view/extension/predicates.go +++ b/view/extension/predicates.go @@ -2,12 +2,13 @@ package extension import ( "fmt" + "sync" + "github.com/viant/datly/utils/types" codec2 "github.com/viant/datly/view/extension/codec" "github.com/viant/xdatly/codec" "github.com/viant/xdatly/predicate" "github.com/viant/xreflect" - "sync" ) const ( @@ -32,6 +33,7 @@ const ( PredicateExists = "exists" PredicateNotExists = "not_exists" + PredicateExpr = "expr" PredicateCriteriaExists = "exists_criteria" PredicateCriteriaNotExists = "not_exists_criteria" PredicateCriteriaIn = "in_criteria" @@ -225,6 +227,10 @@ func NewEqualPredicate() *Predicate { return binaryPredicate(PredicateEqual, "=") } +func NewColumnExpressionPredicate() *Predicate { + return binaryPredicate(PredicateEqual, "=") +} + func NewLessOrEqualPredicate() *Predicate { return binaryPredicate(PredicateLessOrEqual, "<=") } @@ -333,6 +339,10 @@ func NewLikePredicate() *Predicate { return newLikePredicate(PredicateLike, true) } +func NewExprPredicate() *Predicate { + return newExprPredicate(PredicateExpr) +} + func NewNotLikePredicate() *Predicate { return newLikePredicate(PredicateNotLike, false) } @@ -362,6 +372,23 @@ func newLikePredicate(name string, inclusive bool) *Predicate { } } +func newExprPredicate(expr string) *Predicate { + args := []*predicate.NamedArgument{ + { + Name: "Expression", + Position: 0, + }, + } + criteria := fmt.Sprintf(`$criteria.Expression($Expression, $FilterValue)`) + return &Predicate{ + Template: &predicate.Template{ + Name: expr, + Source: " " + criteria, + Args: args, + }, + } +} + func NewContainsPredicate() *Predicate { return newContainsPredicate(PredicateContains, true) } diff --git a/view/predicate.go b/view/predicate.go index 69eb438f4..415550c28 100644 --- a/view/predicate.go +++ b/view/predicate.go @@ -3,6 +3,10 @@ package view import ( "context" "fmt" + "reflect" + "strings" + "sync" + expand "github.com/viant/datly/service/executor/expand" "github.com/viant/datly/utils/types" "github.com/viant/datly/view/extension" @@ -12,9 +16,6 @@ import ( "github.com/viant/xdatly/predicate" "github.com/viant/xreflect" "github.com/viant/xunsafe" - "reflect" - "strings" - "sync" ) type ( @@ -34,6 +35,7 @@ type ( state *expand.NamedVariable hasStateName *expand.NamedVariable handler codec.PredicateHandler + stateType *structology.StateType } PredicateEvaluator struct { @@ -41,6 +43,7 @@ type ( evaluator *expand.Evaluator valueState *expand.NamedVariable hasValueState *expand.NamedVariable + stateType *structology.StateType } ) @@ -51,20 +54,34 @@ func (e *PredicateEvaluator) Compute(ctx context.Context, value interface{}) (*c } val := ctx.Value(expand.PredicateState) - aState := val.(*structology.State) - offset := len(cuxtomCtx.DataUnit.ParamsGroup) - evaluate, err := e.Evaluate(cuxtomCtx, aState, value) + var aState *structology.State + if s, ok := val.(*structology.State); ok { + aState = s + } + if aState == nil && e.stateType != nil { + // Initialize state if absent; do not override if provided. + aState = e.stateType.NewState() + } + // evaluate predicate with an isolated DataUnit to avoid + // mutating parent DataUnit and relying on Shrink/restore across nesting. + var metaSource expand.Dber + if cuxtomCtx.DataUnit != nil { + metaSource = cuxtomCtx.DataUnit.MetaSource + } + isolatedDU := expand.NewDataUnit(metaSource) + tmpCtx := *cuxtomCtx + tmpCtx.DataUnit = isolatedDU + + evaluate, err := e.Evaluate(&tmpCtx, aState, value) if err != nil { return nil, err } - placeholderLen := len(evaluate.DataUnit.ParamsGroup) - offset - var values = make([]interface{}, placeholderLen) - if placeholderLen > 0 { - copy(values, evaluate.DataUnit.ParamsGroup[offset:]) - } + // Collect placeholders from the isolated DataUnit and return them + // to the caller; do not mutate the parent DataUnit here. + values := make([]interface{}, len(isolatedDU.ParamsGroup)) + copy(values, isolatedDU.ParamsGroup) criteria := &codec.Criteria{Expression: evaluate.Buffer.String(), Placeholders: values} - cuxtomCtx.DataUnit.ParamsGroup = cuxtomCtx.DataUnit.ParamsGroup[:offset] return criteria, nil } @@ -150,6 +167,7 @@ func (p *predicateEvaluatorProvider) new(predicateConfig *extension.PredicateCon evaluator: p.evaluator, valueState: p.state, hasValueState: p.hasStateName, + stateType: p.stateType, }, nil } @@ -207,5 +225,6 @@ func (p *predicateEvaluatorProvider) init(resource *Resource, predicateConfig *e p.signature = argsIndexed p.state = stateVariable p.hasStateName = hasVariable + p.stateType = stateType return nil } diff --git a/view/resource.go b/view/resource.go index 94ca4db9a..94cc59435 100644 --- a/view/resource.go +++ b/view/resource.go @@ -72,7 +72,8 @@ type ( Substitutes Substitutes Docs *Documentation - FSEmbedder *state.FSEmbedder + + FSEmbedder *state.FSEmbedder modTime time.Time _doc docs.Service @@ -152,6 +153,17 @@ func (r *Resource) ReverseSubstitutes(text string) string { return r.Substitutes.ReverseReplace(text) } +func (r *Resource) EmbedFS() *embed.FS { + if r.FSEmbedder == nil { + return nil + } + return r.FSEmbedder.EmbedFS() +} + +func (r *Resource) SetFSEmbedder(embedder *state.FSEmbedder) { + r.FSEmbedder = embedder +} + func (r *Resource) SetFs(fs afs.Service) { r.fs = fs } diff --git a/view/state.go b/view/state.go index 3484de7f8..8bfd3715b 100644 --- a/view/state.go +++ b/view/state.go @@ -1,12 +1,14 @@ package view import ( + "strings" + "sync" + "github.com/viant/datly/view/state/predicate" "github.com/viant/sqlx/io/read/cache" "github.com/viant/structology" "github.com/viant/tagly/format/text" - "strings" - "sync" + "github.com/viant/xdatly/handler/state" ) // Statelet allows customizing View fetched from Database @@ -14,9 +16,18 @@ type ( //InputType represents view state Statelet struct { - Template *structology.State - QuerySelector + //SELECTORS + DatabaseFormat text.CaseFormat + OutputFormat text.CaseFormat + Template *structology.State + state.QuerySelector QuerySettings + filtersMu sync.Mutex + initialized bool + _columnNames map[string]bool + result *cache.ParmetrizedQuery + predicate.Filters + Ignore bool } QuerySettings struct { @@ -24,41 +35,8 @@ type ( SyncFlag bool ContentFormat string } - - QuerySelector struct { - //SELECTORS - DatabaseFormat text.CaseFormat - OutputFormat text.CaseFormat - Columns []string `json:",omitempty"` - Fields []string `json:",omitempty"` - OrderBy string `json:",omitempty"` - Offset int `json:",omitempty"` - Limit int `json:",omitempty"` - - Criteria string `json:",omitempty"` - Placeholders []interface{} `json:",omitempty"` - Page int - Ignore bool - predicate.Filters - - initialized bool - _columnNames map[string]bool - result *cache.ParmetrizedQuery - } ) -func (s *QuerySelector) CurrentLimit() int { - return s.Limit -} - -func (s *QuerySelector) CurrentOffset() int { - return s.Offset -} - -func (s *QuerySelector) CurrentPage() int { - return s.Page -} - // Init initializes Statelet func (s *Statelet) Init(aView *View) { if aView != nil && s.Template == nil && aView.Template.stateType != nil { @@ -71,12 +49,12 @@ func (s *Statelet) Init(aView *View) { } // Has checks if Field is present in Template.Columns -func (s *QuerySelector) Has(field string) bool { +func (s *Statelet) Has(field string) bool { _, ok := s._columnNames[field] return ok } -func (s *QuerySelector) Add(fieldName string, isHolder bool) { +func (s *Statelet) Add(fieldName string, isHolder bool) { toLower := strings.ToLower(fieldName) if _, ok := s._columnNames[toLower]; ok { return @@ -94,18 +72,21 @@ func (s *QuerySelector) Add(fieldName string, isHolder bool) { } } -func (s *QuerySelector) SetCriteria(expanded string, placeholders []interface{}) { - s.Criteria = expanded - s.Placeholders = placeholders +// AppendFilters safely appends filters to the selector's Filters to avoid data races. +func (s *Statelet) AppendFilters(filters predicate.Filters) { + if len(filters) == 0 { + return + } + s.filtersMu.Lock() + s.Filters = append(s.Filters, filters...) + s.filtersMu.Unlock() } // NewStatelet creates a selector func NewStatelet() *Statelet { return &Statelet{ - QuerySelector: QuerySelector{ - _columnNames: map[string]bool{}, - initialized: true, - }, + _columnNames: map[string]bool{}, + initialized: true, } } @@ -116,7 +97,7 @@ type State struct { } // QuerySelector returns query selector -func (s *State) QuerySelector(view *View) *QuerySelector { +func (s *State) QuerySelector(view *View) *state.QuerySelector { statelet := s.Lookup(view) if statelet == nil { return nil diff --git a/view/state/hook.go b/view/state/hook.go index 9ae5b49c4..e871f4d6d 100644 --- a/view/state/hook.go +++ b/view/state/hook.go @@ -1,6 +1,11 @@ package state -import "context" +import ( + "context" + + "github.com/viant/xdatly/handler/http" + "github.com/viant/xdatly/handler/state" +) // Initializer is an interface that should be implemented by any type that needs to be initialized type Initializer interface { @@ -11,3 +16,7 @@ type Initializer interface { type Finalizer interface { Finalize(ctx context.Context) error } + +type InjectorFinalizer interface { + Finalize(ctx context.Context, getInjector func(ctx context.Context, path http.Route) (state.Injector, error)) error +} diff --git a/view/state/kind/locator.go b/view/state/kind/locator.go index 57f765f0e..864d85b00 100644 --- a/view/state/kind/locator.go +++ b/view/state/kind/locator.go @@ -2,6 +2,8 @@ package kind import ( "context" + "reflect" + "github.com/viant/datly/view/state" ) @@ -9,7 +11,7 @@ import ( type Locator interface { //Value returns parameter value - Value(ctx context.Context, name string) (interface{}, bool, error) + Value(ctx context.Context, rType reflect.Type, name string) (interface{}, bool, error) //Names returns names of supported parameters Names() []string diff --git a/view/state/kind/locator/body.go b/view/state/kind/locator/body.go index fc41a49aa..e17af401b 100644 --- a/view/state/kind/locator/body.go +++ b/view/state/kind/locator/body.go @@ -3,13 +3,16 @@ package locator import ( "context" "fmt" + "mime" + "mime/multipart" + "net/http" + "reflect" + "sync" + "github.com/viant/datly/shared" "github.com/viant/datly/view/state/kind" "github.com/viant/structology" hstate "github.com/viant/xdatly/handler/state" - "net/http" - "reflect" - "sync" ) type Body struct { @@ -21,22 +24,39 @@ type Body struct { request *http.Request err error sync.Once + isMultipart bool } +const maxMultipartMemory = 32 << 20 // 32 MiB + func (r *Body) Names() []string { return nil } -func (r *Body) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (r *Body) Value(ctx context.Context, rType reflect.Type, name string) (interface{}, bool, error) { var err error - r.Once.Do(func() { - var request *http.Request - request, r.err = shared.CloneHTTPRequest(r.request) - r.body, r.err = readRequestBody(request) - if len(r.body) > 0 { - r.err = r.ensureRequest() + r.initOnce() + var requestState *structology.State + + // Multipart handling + if r.isMultipart { + return r.handleMultipartValue(rType, name) + } + + if len(r.body) > 0 { + if r.requestState != nil && r.requestState.Type().Type() == rType { + requestState = r.requestState } - }) + if name == "" { + requestState, r.err = r.ensureRequest(rType) + } else { + requestState, r.err = r.ensureRequest(r.bodyType) + } + if r.err == nil { + r.requestState = requestState + } + } + if len(r.body) == 0 { return nil, false, nil } @@ -47,16 +67,82 @@ func (r *Body) Value(ctx context.Context, name string) (interface{}, bool, error return r.decodeBodyMap(ctx) } if name == "" { - return r.requestState.State(), true, nil + return requestState.State(), true, nil } - sel, err := r.requestState.Selector(name) + sel, err := requestState.Selector(name) if err != nil { return nil, false, err } - if !sel.Has(r.requestState.Pointer()) { + if !sel.Has(requestState.Pointer()) { + return nil, false, nil + } + return sel.Value(requestState.Pointer()), true, nil +} + +// initOnce initializes body locator state based on content type (multipart vs non-multipart) +func (r *Body) initOnce() { + r.Once.Do(func() { + // Multipart branch + if r.request != nil { + ct := r.request.Header.Get("Content-Type") + if shared.IsMultipartContentType(ct) { + r.isMultipart = true + if mediaType, _, err := mime.ParseMediaType(ct); err == nil && shared.IsFormData(mediaType) { + r.err = r.request.ParseMultipartForm(maxMultipartMemory) + if r.err == nil { + r.seedFormFromMultipart() + } + } + return + } + } + // Non-multipart: clone and read body safely + var request *http.Request + request, r.err = shared.CloneHTTPRequest(r.request) + r.body, r.err = readRequestBody(request) + }) +} + +// handleMultipartValue returns value for multipart/form-data content +func (r *Body) handleMultipartValue(rType reflect.Type, name string) (interface{}, bool, error) { + if r.err != nil { + return nil, false, r.err + } + if r.request == nil || r.request.MultipartForm == nil { return nil, false, nil } - return sel.Value(r.requestState.Pointer()), true, nil + if name == "" { + return nil, false, nil + } + // File destinations + if rType != nil { + // []*multipart.FileHeader + if rType.Kind() == reflect.Slice && rType.Elem() == reflect.TypeOf((*multipart.FileHeader)(nil)) { + files := r.request.MultipartForm.File[name] + if len(files) == 0 { + return nil, false, nil + } + return files, true, nil + } + // *multipart.FileHeader + if rType == reflect.TypeOf((*multipart.FileHeader)(nil)) { + files := r.request.MultipartForm.File[name] + if len(files) == 0 { + return nil, false, nil + } + return files[0], true, nil + } + } + // Textual parts + if r.request.MultipartForm.Value != nil { + if vs, ok := r.request.MultipartForm.Value[name]; ok && len(vs) > 0 { + if rType != nil && rType.Kind() == reflect.Slice && rType.Elem().Kind() == reflect.String { + return vs, true, nil + } + return vs[0], true, nil + } + } + return nil, false, nil } func (r *Body) decodeBodyMap(ctx context.Context) (interface{}, bool, error) { @@ -74,34 +160,45 @@ func (r *Body) decodeBodyMap(ctx context.Context) (interface{}, bool, error) { // NewBody returns body locator func NewBody(opts ...Option) (kind.Locator, error) { options := NewOptions(opts) - if options.BodyType == nil { - return nil, fmt.Errorf("body type was empty") - } if options.request == nil { return nil, fmt.Errorf("request was empty") } if options.Unmarshal == nil { return nil, fmt.Errorf("unmarshal was empty") } + // Allow missing BodyType only for multipart/* requests; otherwise keep existing requirement. + if options.BodyType == nil { + ct := "" + if options.request != nil && options.request.Header != nil { + ct = options.request.Header.Get("Content-Type") + } + isMultipart := false + if ct != "" { + isMultipart = shared.IsMultipartContentType(ct) + } + if !isMultipart { + return nil, fmt.Errorf("body type was empty") + } + } var ret = &Body{request: options.request, bodyType: options.BodyType, unmarshal: options.UnmarshalFunc(), form: options.Form} return ret, nil } -func (r *Body) ensureRequest() (err error) { - if r.bodyType == nil { - return nil +func (r *Body) ensureRequest(rType reflect.Type) (*structology.State, error) { + if rType == nil { + return nil, nil } - rType := r.bodyType if rType.Kind() == reflect.Map { - return nil + return nil, nil } - bodyType := structology.NewStateType(r.bodyType) - r.requestState = bodyType.NewState() - dest := r.requestState.StatePtr() - if err = r.unmarshal(r.body, dest); err == nil { - r.requestState.Sync() + bodyType := structology.NewStateType(rType) + requestState := bodyType.NewState() + dest := requestState.StatePtr() + err := r.unmarshal(r.body, dest) + if err == nil { + requestState.Sync() } - return err + return requestState, err } func (r *Body) updateQueryString(ctx context.Context, body interface{}) { @@ -136,3 +233,19 @@ func (r *Body) updateQueryString(ctx context.Context, body interface{}) { // Encode the query string and assign it back to the request's URL req.URL.RawQuery = q.Encode() } + +// isMultipartRequest checks content type for multipart/form-data +// removed: local isMultipartRequest; use shared.IsMultipartContentType instead + +// seedFormFromMultipart copies textual multipart values into shared form to avoid re-parsing later +func (r *Body) seedFormFromMultipart() { + if r.request == nil || r.request.MultipartForm == nil || r.form == nil { + return + } + for k, vs := range r.request.MultipartForm.Value { + if len(vs) == 0 { + continue + } + r.form.Set(k, vs...) + } +} diff --git a/view/state/kind/locator/constants.go b/view/state/kind/locator/constants.go index f6cd81eeb..516e7c4a2 100644 --- a/view/state/kind/locator/constants.go +++ b/view/state/kind/locator/constants.go @@ -3,6 +3,7 @@ package locator import ( "context" "github.com/viant/datly/view/state/kind" + "reflect" "sync" ) @@ -16,7 +17,7 @@ func (r *Constants) Names() []string { return nil } -func (r *Constants) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (r *Constants) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { if len(r.constants) > 0 { if value, ok := r.constants[name]; ok { return value, true, nil diff --git a/view/state/kind/locator/context.go b/view/state/kind/locator/context.go index d5fcd827c..33d0985c4 100644 --- a/view/state/kind/locator/context.go +++ b/view/state/kind/locator/context.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/viant/datly/view/state/kind" "github.com/viant/xdatly/handler/exec" + "reflect" ) type Context struct { @@ -14,7 +15,7 @@ func (v *Context) Names() []string { return nil } -func (v *Context) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (v *Context) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { rawValue := ctx.Value(exec.ContextKey) if rawValue == nil { diff --git a/view/state/kind/locator/cookie.go b/view/state/kind/locator/cookie.go index 804949592..117550935 100644 --- a/view/state/kind/locator/cookie.go +++ b/view/state/kind/locator/cookie.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/viant/datly/view/state/kind" "net/http" + "reflect" ) type Cookie struct { @@ -19,7 +20,7 @@ func (v *Cookie) Names() []string { return result } -func (v *Cookie) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (v *Cookie) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { for _, cookie := range v.cookies { if cookie.Name == name { return cookie.Value, true, nil diff --git a/view/state/kind/locator/data.go b/view/state/kind/locator/data.go index efcabfdb5..db35876c5 100644 --- a/view/state/kind/locator/data.go +++ b/view/state/kind/locator/data.go @@ -18,7 +18,7 @@ func (p *DataView) Names() []string { return nil } -func (p *DataView) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (p *DataView) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { aView, ok := p.Views[name] if !ok { return nil, false, fmt.Errorf("failed to lookup view: %v", name) diff --git a/view/state/kind/locator/env.go b/view/state/kind/locator/env.go index 05ccc5217..28b980e5d 100644 --- a/view/state/kind/locator/env.go +++ b/view/state/kind/locator/env.go @@ -4,6 +4,7 @@ import ( "context" "github.com/viant/datly/view/state/kind" "os" + "reflect" ) type Env struct { @@ -14,7 +15,7 @@ func (v *Env) Names() []string { return os.Environ() } -func (v *Env) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (v *Env) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { ret, ok := v.env[name] return ret, ok, nil } diff --git a/view/state/kind/locator/form.go b/view/state/kind/locator/form.go index 387815e79..2b9190533 100644 --- a/view/state/kind/locator/form.go +++ b/view/state/kind/locator/form.go @@ -2,29 +2,76 @@ package locator import ( "context" + "mime" + "mime/multipart" + "net/http" + "net/url" + "reflect" + "sync" + + "github.com/viant/datly/shared" "github.com/viant/datly/view/state/kind" "github.com/viant/xdatly/handler/state" - "net/http" ) type Form struct { form *state.Form request *http.Request + once sync.Once } func (r *Form) Names() []string { return nil } -func (r *Form) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (r *Form) Value(ctx context.Context, rType reflect.Type, name string) (interface{}, bool, error) { if r.form != nil && len(r.form.Values) == 0 && r.request == nil { return nil, false, nil } + + // Support file uploads when parameters are declared with kind=form + // and types *multipart.FileHeader or []*multipart.FileHeader. This + // aligns multipart file fields with form semantics instead of body. + if r.request != nil && shared.IsMultipartContentType(r.request.Header.Get("Content-Type")) && rType != nil { + // Parse/seed multipart values only once + r.once.Do(func() { r.seedFormFromMultipart() }) + if r.request.MultipartForm != nil { + // []*multipart.FileHeader + if rType.Kind() == reflect.Slice && rType.Elem() == reflect.TypeOf((*multipart.FileHeader)(nil)) { + files := r.request.MultipartForm.File[name] + if len(files) == 0 { + return nil, false, nil + } + return files, true, nil + } + // *multipart.FileHeader + if rType == reflect.TypeOf((*multipart.FileHeader)(nil)) { + files := r.request.MultipartForm.File[name] + if len(files) == 0 { + return nil, false, nil + } + return files[0], true, nil + } + } + } + values, ok := r.form.Lookup(name) if !ok { if r.request == nil { return nil, false, nil } + // If multipart, seed from multipart and avoid FormValue fallback + if shared.IsMultipartContentType(r.request.Header.Get("Content-Type")) { + r.once.Do(func() { r.seedFormFromMultipart() }) + if values, ok = r.form.Lookup(name); ok { + if len(values) > 1 { + return values, true, nil + } + return r.form.Get(name), true, nil + } + return nil, false, nil + } + // Non-multipart: use standard FormValue fallback r.form.Mutex().Lock() defer r.form.Mutex().Unlock() value := r.request.FormValue(name) @@ -49,3 +96,36 @@ func NewForm(opts ...Option) (kind.Locator, error) { var ret = &Form{form: options.Form, request: options.request} return ret, nil } + +// seedFormFromMultipart parses multipart/form-data (if needed) and copies textual values to the shared form +func (r *Form) seedFormFromMultipart() { + if r.request == nil || r.form == nil { + return + } + if r.request.MultipartForm == nil && len(r.form.Values) == 0 { + // Only ParseMultipartForm for form-data; other multipart types aren't + // supported by ParseMultipartForm. If the shared form already has + // values, treat it as authoritative and avoid parsing. + ct := r.request.Header.Get("Content-Type") + if ct != "" { + if mediaType, _, err := mime.ParseMediaType(ct); err == nil && shared.IsFormData(mediaType) { + // Use the same default memory threshold as Body locator + const maxMultipartMemory = 32 << 20 // 32 MiB + _ = r.request.ParseMultipartForm(maxMultipartMemory) + } + } + } + if r.request.MultipartForm == nil { + return + } + if len(r.request.Form) == 0 { + r.request.Form = url.Values{} + } + for k, vs := range r.request.MultipartForm.Value { + if len(vs) == 0 { + continue + } + r.form.Set(k, vs...) + r.request.Form[k] = vs + } +} diff --git a/view/state/kind/locator/generator.go b/view/state/kind/locator/generator.go index 1583357f8..f020b40ff 100644 --- a/view/state/kind/locator/generator.go +++ b/view/state/kind/locator/generator.go @@ -4,6 +4,7 @@ import ( "context" "github.com/google/uuid" "github.com/viant/datly/view/state/kind" + "reflect" "strings" "time" ) @@ -14,7 +15,7 @@ func (v *Generator) Names() []string { return nil } -func (v *Generator) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (v *Generator) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { switch strings.ToLower(name) { case "nil": return nil, true, nil diff --git a/view/state/kind/locator/header.go b/view/state/kind/locator/header.go index e6a8135e2..c2fd3962f 100644 --- a/view/state/kind/locator/header.go +++ b/view/state/kind/locator/header.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/viant/datly/view/state/kind" "net/http" + "reflect" ) type Header struct { @@ -20,7 +21,7 @@ func (q *Header) Names() []string { return result } -func (q *Header) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (q *Header) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { value, ok := q.header[name] if !ok { return nil, false, nil diff --git a/view/state/kind/locator/http.go b/view/state/kind/locator/http.go index a2becfe56..62c9ed697 100644 --- a/view/state/kind/locator/http.go +++ b/view/state/kind/locator/http.go @@ -4,10 +4,12 @@ import ( "bytes" "context" "fmt" - "github.com/viant/datly/view/state/kind" "io" "net/http" + "reflect" "strings" + + "github.com/viant/datly/view/state/kind" ) type HttpRequest struct { @@ -19,7 +21,7 @@ func (p *HttpRequest) Names() []string { return nil } -func (p *HttpRequest) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (p *HttpRequest) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { request := p.request if p.request == nil { var err error diff --git a/view/state/kind/locator/object.go b/view/state/kind/locator/object.go index f1c066718..ced3fe835 100644 --- a/view/state/kind/locator/object.go +++ b/view/state/kind/locator/object.go @@ -6,6 +6,7 @@ import ( "github.com/viant/datly/view/state" "github.com/viant/datly/view/state/kind" "github.com/viant/structology" + "reflect" ) type Object struct { @@ -19,7 +20,7 @@ func (p *Object) Names() []string { return nil } -func (p *Object) Value(ctx context.Context, names string) (interface{}, bool, error) { +func (p *Object) Value(ctx context.Context, _ reflect.Type, names string) (interface{}, bool, error) { parameter := p.matchByLocation(names) if parameter == nil { return nil, false, fmt.Errorf("failed to match parameter by location: %v", names) diff --git a/view/state/kind/locator/options.go b/view/state/kind/locator/options.go index d591e8666..18be24842 100644 --- a/view/state/kind/locator/options.go +++ b/view/state/kind/locator/options.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "reflect" + "sync" "github.com/viant/datly/gateway/router/marshal/config" "github.com/viant/datly/gateway/router/marshal/json" @@ -21,12 +22,14 @@ import ( // Options represents locator options type ( Options struct { - request *http.Request - Form *hstate.Form - Path map[string]string - Query url.Values - Header http.Header - Body []byte + mu sync.RWMutex + request *http.Request + Form *hstate.Form + QuerySelectors hstate.QuerySelectors + Path map[string]string + Query url.Values + Header http.Header + Body []byte fromError error Parent *KindLocator @@ -56,6 +59,9 @@ type ( ) func (o Options) LookupParameters(name string) *state.Parameter { + o.mu.RLock() + defer o.mu.RUnlock() + if len(o.InputParameters) > 0 { if ret, ok := o.InputParameters[name]; ok { return ret @@ -70,10 +76,17 @@ func (o Options) LookupParameters(name string) *state.Parameter { } func (o *Options) GetRequest() (*http.Request, error) { - return shared.CloneHTTPRequest(o.request) + o.mu.RLock() + req := o.request + o.mu.RUnlock() + + return shared.CloneHTTPRequest(req) } func (o *Options) UnmarshalFunc() Unmarshal { + o.mu.Lock() + defer o.mu.Unlock() + if o.Unmarshal != nil { return o.Unmarshal } @@ -100,6 +113,9 @@ var defaultURL, _ = url.Parse("http://localhost:8080/") // WithRequest create http requestState option func WithRequest(request *http.Request) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + ensureValueRequest(request) o.request = request } @@ -117,6 +133,9 @@ func ensureValueRequest(request *http.Request) { // WithCustom creates custom options func WithCustom(options ...interface{}) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Custom = options } } @@ -124,6 +143,9 @@ func WithCustom(options ...interface{}) Option { // WithURIPattern create Path pattern requestState func WithURIPattern(URI string) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.URIPattern = URI } } @@ -131,6 +153,9 @@ func WithURIPattern(URI string) Option { // WithBodyType create Body Type option func WithBodyType(rType reflect.Type) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.BodyType = rType } } @@ -138,6 +163,9 @@ func WithBodyType(rType reflect.Type) Option { // WithUnmarshal creates with unmarshal options func WithUnmarshal(fn func([]byte, interface{}) error) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Unmarshal = fn } } @@ -145,6 +173,9 @@ func WithUnmarshal(fn func([]byte, interface{}) error) Option { // WithParent creates with parent options func WithParent(locators *KindLocator) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Parent = locators } } @@ -152,12 +183,18 @@ func WithParent(locators *KindLocator) Option { // WithParameterLookup creates with parameter options func WithParameterLookup(lookupFn ParameterLookup) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.ParameterLookup = lookupFn } } func WithIOConfig(config *config.IOConfig) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.IOConfig = config } } @@ -165,6 +202,9 @@ func WithIOConfig(config *config.IOConfig) Option { // WithInputParameters creates with parameter options func WithInputParameters(parameters state.NamedParameters) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + if len(o.resourceConstants) == 0 { o.resourceConstants = make(map[string]interface{}) } @@ -181,15 +221,30 @@ func WithInputParameters(parameters state.NamedParameters) Option { } } +func WithQuerySelectors(selectors hstate.QuerySelectors) Option { + return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + + o.QuerySelectors = selectors + } +} + // WithPathParameters create with path parameters options func WithPathParameters(parameters map[string]string) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Path = parameters } } func WithReadInto(fn ReadInto) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.ReadInto = fn } } @@ -197,6 +252,9 @@ func WithReadInto(fn ReadInto) Option { // WithViews returns with views options func WithViews(views view.NamedViews) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Views = views } } @@ -204,12 +262,18 @@ func WithViews(views view.NamedViews) Option { // WithState returns with satte options func WithState(state *structology.State) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.State = state } } func WithOutputParameters(parameters state.Parameters) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.OutputParameters = parameters.Index() } } @@ -217,6 +281,9 @@ func WithOutputParameters(parameters state.Parameters) Option { // WithDispatcher returns options to set dispatcher func WithDispatcher(dispatcher contract.Dispatcher) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Dispatcher = dispatcher } } @@ -224,6 +291,9 @@ func WithDispatcher(dispatcher contract.Dispatcher) Option { // WithView returns options to set view func WithView(aView *view.View) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.View = aView } } @@ -231,6 +301,9 @@ func WithView(aView *view.View) Option { // WithForm return form option func WithForm(form *hstate.Form) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + if o.Form == nil { o.Form = form } else if form != nil { @@ -242,6 +315,9 @@ func WithForm(form *hstate.Form) Option { // WithQuery return query parameters option func WithQuery(parameters url.Values) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + if o.Query == nil { o.Query = parameters } else { @@ -254,6 +330,9 @@ func WithQuery(parameters url.Values) Option { func WithLogger(logger logger.Logger) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Logger = logger } } @@ -261,6 +340,9 @@ func WithLogger(logger logger.Logger) Option { // WithQueryParameter return query parameter option func WithQueryParameter(name, value string) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + if o.Query == nil { o.Query = make(url.Values) } @@ -271,6 +353,9 @@ func WithQueryParameter(name, value string) Option { // WithHeader return header option func WithHeader(name, value string) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + if o.Header == nil { o.Header = make(http.Header) } @@ -281,6 +366,9 @@ func WithHeader(name, value string) Option { // WithHeaders return headers option func WithHeaders(header http.Header) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + if o.Header == nil { o.Header = header } @@ -293,6 +381,9 @@ func WithHeaders(header http.Header) Option { // WithMetrics return metrics option func WithMetrics(metrics response.Metrics) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Metrics = metrics } } @@ -300,6 +391,9 @@ func WithMetrics(metrics response.Metrics) Option { // WithResource return resource option func WithResource(resource *view.Resource) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Resource = resource } } @@ -307,6 +401,9 @@ func WithResource(resource *view.Resource) Option { // WithConstants return Constants option func WithConstants(constants map[string]interface{}) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Constants = constants } } @@ -314,6 +411,9 @@ func WithConstants(constants map[string]interface{}) Option { // WithTypes return types option func WithTypes(types ...*state.Type) Option { return func(o *Options) { + o.mu.Lock() + defer o.mu.Unlock() + o.Types = types } } diff --git a/view/state/kind/locator/parameter.go b/view/state/kind/locator/parameter.go index b46f54ed0..35578c13c 100644 --- a/view/state/kind/locator/parameter.go +++ b/view/state/kind/locator/parameter.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/viant/datly/view/state" "github.com/viant/datly/view/state/kind" + "reflect" ) type Parameter struct { @@ -16,7 +17,7 @@ func (p *Parameter) Names() []string { return nil } -func (p *Parameter) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (p *Parameter) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { parameter, ok := p.Parameters[name] if !ok { return nil, false, fmt.Errorf("uknonw parameter: %s", name) diff --git a/view/state/kind/locator/path.go b/view/state/kind/locator/path.go index eecabec7c..79a8af548 100644 --- a/view/state/kind/locator/path.go +++ b/view/state/kind/locator/path.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/viant/datly/view/state/kind" "github.com/viant/toolbox" + "reflect" ) type Path struct { @@ -20,7 +21,7 @@ func (v *Path) Names() []string { return result } -func (v *Path) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (v *Path) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { if name == "" { return v.path, true, nil } diff --git a/view/state/kind/locator/query.go b/view/state/kind/locator/query.go index 605f1b1a7..b532215aa 100644 --- a/view/state/kind/locator/query.go +++ b/view/state/kind/locator/query.go @@ -7,6 +7,7 @@ import ( "github.com/viant/xdatly/handler/exec" "net/http" "net/url" + "reflect" ) type Query struct { @@ -23,7 +24,7 @@ func (q *Query) Names() []string { return result } -func (q *Query) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (q *Query) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { if name == "" { return q.rawQuery, true, nil } diff --git a/view/state/kind/locator/repeated.go b/view/state/kind/locator/repeated.go index d40972b4f..2f19e91ae 100644 --- a/view/state/kind/locator/repeated.go +++ b/view/state/kind/locator/repeated.go @@ -3,12 +3,13 @@ package locator import ( "context" "fmt" - "github.com/viant/datly/view/state" - "github.com/viant/datly/view/state/kind" - "github.com/viant/xunsafe" "reflect" "sync" "sync/atomic" + + "github.com/viant/datly/view/state" + "github.com/viant/datly/view/state/kind" + "github.com/viant/xunsafe" ) type Repeated struct { @@ -27,7 +28,7 @@ func (p *Repeated) Names() []string { return nil } -func (p *Repeated) Value(ctx context.Context, names string) (interface{}, bool, error) { +func (p *Repeated) Value(ctx context.Context, _ reflect.Type, names string) (interface{}, bool, error) { parameter := p.matchByLocation(names) if parameter == nil { return nil, false, fmt.Errorf("failed to match parameter by location: %v", names) diff --git a/view/state/kind/locator/state.go b/view/state/kind/locator/state.go index bbd44cd4c..503dba3f5 100644 --- a/view/state/kind/locator/state.go +++ b/view/state/kind/locator/state.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/viant/datly/view/state/kind" "github.com/viant/structology" + "reflect" ) type State struct { @@ -14,7 +15,7 @@ type State struct { func (p *State) Names() []string { return nil } -func (p *State) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (p *State) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { _, err := p.State.Selector(name) if err != nil { return nil, false, nil diff --git a/view/state/kind/locator/transient.go b/view/state/kind/locator/transient.go index 0fde22e6e..72ac34cca 100644 --- a/view/state/kind/locator/transient.go +++ b/view/state/kind/locator/transient.go @@ -3,6 +3,7 @@ package locator import ( "context" "github.com/viant/datly/view/state/kind" + "reflect" ) type Transient struct{} @@ -11,7 +12,7 @@ func (v *Transient) Names() []string { return nil } -func (v *Transient) Value(ctx context.Context, name string) (interface{}, bool, error) { +func (v *Transient) Value(ctx context.Context, _ reflect.Type, name string) (interface{}, bool, error) { if name == "" { return nil, false, nil } diff --git a/view/state/parameter.go b/view/state/parameter.go index ac6461b57..a1ded9294 100644 --- a/view/state/parameter.go +++ b/view/state/parameter.go @@ -3,6 +3,11 @@ package state import ( "context" "fmt" + "net/http" + "reflect" + "strconv" + "strings" + "github.com/viant/datly/internal/setter" "github.com/viant/datly/shared" "github.com/viant/datly/utils/types" @@ -11,10 +16,6 @@ import ( "github.com/viant/structology" "github.com/viant/xreflect" "github.com/viant/xunsafe" - "net/http" - "reflect" - "strconv" - "strings" ) type ( @@ -512,7 +513,12 @@ func (p *Parameter) initCodec(resource Resource) error { if p.Output == nil { return nil } - + stateTag, _ := tags.ParseStateTags(reflect.StructTag(p.Tag), resource.EmbedFS()) + if stateTag != nil { + if stateTag.Codec != nil && stateTag.Codec.Body != "" { + p.Output.Body = stateTag.Codec.Body + } + } inputType := p.Schema.Type() if err := p.Output.Init(resource, inputType); err != nil { return err @@ -520,10 +526,9 @@ func (p *Parameter) initCodec(resource Resource) error { if p.Output.Schema == nil { return nil } - if !p.Output.Schema.IsNamed() { fieldTag := reflect.StructTag(p.Tag) - if stateTag, _ := tags.ParseStateTags(fieldTag, nil); stateTag != nil { + if stateTag != nil { stateTag.TypeName = SanitizeTypeName(p.Output.Schema.Name) p.Tag = string(stateTag.UpdateTag(fieldTag)) } diff --git a/view/state/parameters.go b/view/state/parameters.go index 0b503b84a..ff409d1e4 100644 --- a/view/state/parameters.go +++ b/view/state/parameters.go @@ -2,6 +2,10 @@ package state import ( "fmt" + "net/http" + "reflect" + "strings" + "github.com/viant/datly/internal/setter" "github.com/viant/datly/shared" "github.com/viant/datly/utils/types" @@ -13,9 +17,6 @@ import ( "github.com/viant/velty" "github.com/viant/xreflect" "github.com/viant/xunsafe" - "net/http" - "reflect" - "strings" ) const ( diff --git a/view/state/resource.go b/view/state/resource.go index 7c39ad857..b80f1fe1d 100644 --- a/view/state/resource.go +++ b/view/state/resource.go @@ -2,6 +2,7 @@ package state import ( "context" + "embed" "github.com/viant/xdatly/codec" "github.com/viant/xreflect" ) @@ -22,5 +23,9 @@ type ( ExpandSubstitutes(text string) string ReverseSubstitutes(text string) string + + EmbedFS() *embed.FS + + SetFSEmbedder(embedder *FSEmbedder) } ) diff --git a/view/state/type.go b/view/state/type.go index d64c07281..262a83959 100644 --- a/view/state/type.go +++ b/view/state/type.go @@ -4,6 +4,10 @@ import ( "context" "embed" "fmt" + "reflect" + "strings" + "unicode" + "github.com/viant/datly/internal/setter" "github.com/viant/datly/utils/types" "github.com/viant/datly/view/extension" @@ -11,9 +15,6 @@ import ( "github.com/viant/structology" "github.com/viant/tagly/format/text" "github.com/viant/xreflect" - "reflect" - "strings" - "unicode" ) type ( @@ -110,6 +111,10 @@ func (t *Type) ensureEmbedder(reflect.Type) { t.embedder = NewFSEmbedder(nil) } t.embedder.SetType(reflect.TypeOf(t)) + if t.resource != nil && t.resource.EmbedFS() == nil { + t.resource.SetFSEmbedder(t.embedder) + } + } func (t *Type) adjustConstants() { @@ -261,11 +266,16 @@ func BuildSchema(field *reflect.StructField, pTag *tags.Parameter, result *Param isSlice = true rawType = rawType.Elem() } + isPtr := false if rawType.Kind() == reflect.Ptr { rawType = rawType.Elem() + isPtr = true } rawName := rawType.Name() + if isPtr { + rawName = "*" + rawName + } if pTag.Cardinality != "" { result.ensureSchema() result.Schema.Cardinality = Cardinality(pTag.Cardinality) diff --git a/view/state/types.go b/view/state/types.go index 292ad4147..f301290ea 100644 --- a/view/state/types.go +++ b/view/state/types.go @@ -11,6 +11,9 @@ type Types struct { } func (c *Types) Lookup(p reflect.Type) (*Type, bool) { + if len(c.types) == 0 { + return nil, false + } c.RWMutex.RLock() ret, ok := c.types[p] c.RWMutex.RUnlock() diff --git a/view/tags/codec.go b/view/tags/codec.go index a16be7509..d606ed717 100644 --- a/view/tags/codec.go +++ b/view/tags/codec.go @@ -1,9 +1,9 @@ package tags import ( - "fmt" - "github.com/viant/tagly/tags" "strings" + + "github.com/viant/tagly/tags" ) // CodecTag codec tag @@ -31,10 +31,11 @@ func (t *Tag) updatedCodec(key string, value string) (err error) { } tag.Body = string(data) default: + expr := key if value != "" { - return fmt.Errorf("invalid argument %s", value) + expr += " =" + value } - tag.Arguments = append(tag.Arguments, key) + tag.Arguments = append(tag.Arguments, expr) } return err } diff --git a/view/tags/parameter.go b/view/tags/parameter.go index 7acd1c563..de777c815 100644 --- a/view/tags/parameter.go +++ b/view/tags/parameter.go @@ -88,7 +88,7 @@ func (p *Parameter) Tag() *tags.Tag { if *p.Cacheable { value = "true" } - appendNonEmpty(builder, "cachable", value) + appendNonEmpty(builder, "cacheable", value) } if p.Cardinality == "One" { diff --git a/view/tags/parser.go b/view/tags/parser.go index aa4c407a0..283d1eded 100644 --- a/view/tags/parser.go +++ b/view/tags/parser.go @@ -4,14 +4,15 @@ import ( "context" "embed" "fmt" + "reflect" + "strings" + "github.com/viant/afs" "github.com/viant/afs/storage" "github.com/viant/afs/url" "github.com/viant/tagly/format" "github.com/viant/tagly/tags" "github.com/viant/xreflect" - "reflect" - "strings" ) // ValueTag represents default value tag diff --git a/view/tags/predicate.go b/view/tags/predicate.go index e66ef5e76..956a2bc5b 100644 --- a/view/tags/predicate.go +++ b/view/tags/predicate.go @@ -2,9 +2,10 @@ package tags import ( "fmt" - "github.com/viant/tagly/tags" "strconv" "strings" + + "github.com/viant/tagly/tags" ) // PredicateTag Predicate tag @@ -70,10 +71,11 @@ func (t *Tag) updatedPredicate(key string, value string) (err error) { return fmt.Errorf("invalid predicate ensure: %s %w", value, err) } default: + expr := key if value != "" { - return fmt.Errorf("invalid argument %s", value) + expr = key + "=" + value } - tag.Arguments = append(tag.Arguments, key) + tag.Arguments = append(tag.Arguments, expr) } return err } diff --git a/view/template.go b/view/template.go index c0b523b6c..ae4308723 100644 --- a/view/template.go +++ b/view/template.go @@ -231,6 +231,10 @@ func (t *Template) EvaluateState(ctx context.Context, parameterState *structolog } func (t *Template) EvaluateStateWithSession(ctx context.Context, parameterState *structology.State, parentParam *expand.ViewContext, batchData *BatchData, sess *extension.Session, options ...interface{}) (*expand.State, error) { + // Ensure parameter state is initialized when absent, but never override an existing one. + if parameterState == nil && t.stateType != nil { + parameterState = t.stateType.NewState() + } var expander expand.Expander var dataUnit *expand.DataUnit for _, option := range options { @@ -372,8 +376,7 @@ func (t *Template) Expand(placeholders *[]interface{}, SQL string, selector *Sta if value.Key == "?" { placeholder, err := sanitized.Next() if err != nil { - return "", fmt.Errorf("failed to get placeholder: %w, SQL: %v, values: %v\n", err, SQL, values) - + return "", fmt.Errorf("failed to get placeholder: %w, SQL: %v, values: %+v\n", err, SQL, values) } *placeholders = append(*placeholders, placeholder) continue diff --git a/view/view.go b/view/view.go index 42bcc1906..9274c8053 100644 --- a/view/view.go +++ b/view/view.go @@ -4,6 +4,12 @@ import ( "context" "database/sql" "fmt" + "net/http" + "path" + "reflect" + "strings" + "time" + "github.com/viant/afs/url" "github.com/viant/datly/gateway/router/marshal" "github.com/viant/datly/internal/setter" @@ -23,11 +29,6 @@ import ( "github.com/viant/tagly/format/text" "github.com/viant/xreflect" "github.com/viant/xunsafe" - "net/http" - "path" - "reflect" - "strings" - "time" ) const ( @@ -154,15 +155,16 @@ func (v *View) Context(ctx context.Context) context.Context { // Constraints configure what can be selected by Statelet // For each _field, default value is `false` type Constraints struct { - Criteria bool - OrderBy bool - Limit bool - Offset bool - Projection bool //enables columns projection from client (default ${NS}_fields= query param) - Filterable []string - SQLMethods []*Method `json:",omitempty"` - _sqlMethods map[string]*Method - Page *bool + Criteria bool + OrderBy bool + OrderByColumn map[string]string + Limit bool + Offset bool + Projection bool //enables columns projection from client (default ${NS}_fields= query param) + Filterable []string + SQLMethods []*Method `json:",omitempty"` + _sqlMethods map[string]*Method + Page *bool } func (v *View) Resource() state.Resource { @@ -400,6 +402,10 @@ func (v *View) inheritRelationsFromTag(schema *state.Schema) error { refViewOptions = append(refViewOptions, WithCache(aCache)) } + if viewTag.Limit != nil { + viewOptions = append(viewOptions, WithLimit(viewTag.Limit)) + } + if viewTag.PublishParent { refViewOptions = append(refViewOptions, WithViewPublishParent(viewTag.PublishParent)) } @@ -473,6 +479,9 @@ func WithLimit(limit *int) Option { } view.Selector.Constraints.Limit = true view.Selector.Limit = *limit + if limit != nil { + view.Selector.NoLimit = *limit == 0 + } return nil } }