diff --git a/.claude/skills/stackql-provider-development.md b/.claude/skills/stackql-provider-development.md index 75d1398..9ff7359 100644 --- a/.claude/skills/stackql-provider-development.md +++ b/.claude/skills/stackql-provider-development.md @@ -521,6 +521,238 @@ sqlExternalTables: precision: integer ``` +### Query Parameter Pushdown (`queryParamPushdown`) + +Enables SQL clause pushdown to API query parameters. Supports OData and custom API dialects for filter, projection, ordering, and limit operations. + +**Location:** Can be set at **provider, providerService, service, resource, or method level**. Config inherits from higher levels, with lower levels overriding higher levels: + +``` +Method -> Resource -> Service -> ProviderService -> Provider +``` + +This allows you to set a default OData config at the service level and have all resources/methods inherit it, while still allowing specific methods to override with custom settings. + +```yaml +x-stackQL-config: + queryParamPushdown: + # Column projection (SELECT clause pushdown) + select: + dialect: odata | custom # "custom" is default; "odata" applies OData defaults + paramName: "$select" # Not required for OData (default: "$select") + delimiter: "," # Not required for OData (default: ",") + supportedColumns: # Optional, omit or ["*"] for all columns + - "id" + - "name" + - "status" + + # Row filtering (WHERE clause pushdown) + filter: + dialect: odata | custom # "custom" is default; "odata" applies OData defaults + paramName: "$filter" # Not required for OData (default: "$filter") + syntax: odata # Not required for OData (default: "odata") + supportedOperators: # Required - which operators can be pushed down + - "eq" + - "ne" + - "gt" + - "lt" + - "ge" + - "le" + - "contains" + - "startswith" + supportedColumns: # Optional, omit or ["*"] for all columns + - "displayName" + - "status" + - "createdDate" + + # Ordering (ORDER BY clause pushdown) + orderBy: + dialect: odata | custom # "custom" is default; "odata" applies OData defaults + paramName: "$orderby" # Not required for OData (default: "$orderby") + syntax: odata # Not required for OData (default: "odata") + supportedColumns: # Optional, omit or ["*"] for all columns + - "name" + - "createdDate" + + # Row limit (LIMIT clause pushdown) + top: + dialect: odata | custom # "custom" is default; "odata" applies OData defaults + paramName: "$top" # Not required for OData (default: "$top") + maxValue: 1000 # Optional, cap on pushdown value + + # Count (SELECT COUNT(*) pushdown) + count: + dialect: odata | custom # "custom" is default; "odata" applies OData defaults + paramName: "$count" # Not required for OData (default: "$count") + paramValue: "true" # Not required for OData (default: "true") + responseKey: "@odata.count"# Not required for OData (default: "@odata.count") +``` + +**Minimal OData Configuration:** + +When using OData dialect, defaults are applied automatically: + +```yaml +x-stackQL-config: + queryParamPushdown: + select: {} + filter: + dialect: odata + supportedOperators: ["eq", "ne", "gt", "lt", "contains"] + orderBy: + dialect: odata + top: + dialect: odata + count: + dialect: odata +``` + +**Custom API Configuration:** + +For APIs with custom query parameter names: + +```yaml +x-stackQL-config: + queryParamPushdown: + select: + paramName: "fields" + delimiter: "," + filter: + paramName: "filter" + syntax: "key_value" # filter[status]=active&filter[region]=us-east-1 + supportedOperators: + - "eq" + supportedColumns: + - "status" + - "region" + orderBy: + paramName: "sort" + syntax: "prefix" # sort=-createdAt (prefix - for desc) + supportedColumns: + - "createdAt" + - "name" + top: + paramName: "limit" + maxValue: 100 + count: + paramName: "include_count" + paramValue: "1" + responseKey: "meta.total" +``` + +**Supported Filter Syntaxes:** + +| Syntax | Example Output | Use Case | +|--------|---------------|----------| +| `odata` | `$filter=status eq 'active' and region eq 'us-east-1'` | OData APIs | +| `key_value` | `filter[status]=active&filter[region]=us-east-1` | Rails-style APIs | +| `simple` | `status=active®ion=us-east-1` | Basic query params | + +**Supported OrderBy Syntaxes:** + +| Syntax | Example | Notes | +|--------|---------|-------| +| `odata` | `$orderby=name desc,date asc` | Space-separated direction | +| `prefix` | `sort=-name,+date` | `-` for desc, `+` or none for asc | +| `suffix` | `sort=name:desc,date:asc` | Colon-separated direction | + +**Column/Operator Support Logic:** + +| Value | Behavior | +|-------|----------| +| omitted / `null` | All items allowed | +| `["*"]` | Explicit "all items" (same as omitted) | +| `["col1", "col2"]` | Only these items supported | +| `[]` | No items supported (effectively disabled) | + +**OData Example with Inheritance (TripPin Reference Service):** + +Set a default config at service level, then override at resource or method level as needed: + +```yaml +# Service-level config - inherited by all resources/methods +x-stackQL-config: + queryParamPushdown: + select: + dialect: odata + filter: + dialect: odata + supportedOperators: + - "eq" + - "ne" + orderBy: + dialect: odata + top: + dialect: odata + count: + dialect: odata + +components: + x-stackQL-resources: + # Inherits service-level config (no override needed) + airlines: + id: odata.trippin.airlines + name: airlines + methods: + list: + operation: + $ref: '#/paths/~1Airlines/get' + # No config - inherits from service level + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/airlines/methods/list' + + # Resource-level override with full operator support + people: + id: odata.trippin.people + name: people + config: + queryParamPushdown: + filter: + dialect: odata + supportedOperators: + - "eq" + - "ne" + - "gt" + - "lt" + - "contains" + - "startswith" + methods: + list: + operation: + $ref: '#/paths/~1People/get' + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/people/methods/list' + + # Method-level override with restricted columns + airports: + id: odata.trippin.airports + name: airports + methods: + list: + operation: + $ref: '#/paths/~1Airports/get' + config: + queryParamPushdown: + select: + dialect: odata + supportedColumns: + - "Name" + - "IcaoCode" + filter: + dialect: odata + supportedColumns: + - "Name" + - "IcaoCode" + top: + dialect: odata + maxValue: 100 + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/airports/methods/list' +``` + --- ## Method Definition diff --git a/anysdk/config.go b/anysdk/config.go index d6c9e98..773bed7 100644 --- a/anysdk/config.go +++ b/anysdk/config.go @@ -21,6 +21,7 @@ type StackQLConfig interface { GetVariations() (Variations, bool) GetViews() map[string]View GetExternalTables() map[string]SQLExternalTable + GetQueryParamPushdown() (QueryParamPushdown, bool) // isObjectSchemaImplicitlyUnioned() bool setResource(rsc Resource) @@ -36,6 +37,7 @@ type standardStackQLConfig struct { Views map[string]*standardViewContainer `json:"views" yaml:"views"` ExternalTables map[string]standardSQLExternalTable `json:"sqlExternalTables" yaml:"sqlExternalTables"` Auth *standardAuthDTO `json:"auth,omitempty" yaml:"auth,omitempty"` + QueryParamPushdown *standardQueryParamPushdown `json:"queryParamPushdown,omitempty" yaml:"queryParamPushdown,omitempty"` } func (qt standardStackQLConfig) JSONLookup(token string) (interface{}, error) { @@ -48,6 +50,8 @@ func (qt standardStackQLConfig) JSONLookup(token string) (interface{}, error) { return qt.RequestTranslate, nil case "views": return qt.Views, nil + case "queryParamPushdown": + return qt.QueryParamPushdown, nil default: return nil, fmt.Errorf("could not resolve token '%s' from QueryTranspose doc object", token) } @@ -126,6 +130,13 @@ func (cfg *standardStackQLConfig) GetAuth() (AuthDTO, bool) { return cfg.Auth, cfg.Auth != nil } +func (cfg *standardStackQLConfig) GetQueryParamPushdown() (QueryParamPushdown, bool) { + if cfg.QueryParamPushdown == nil { + return nil, false + } + return cfg.QueryParamPushdown, true +} + func (cfg *standardStackQLConfig) GetExternalTables() map[string]SQLExternalTable { rv := make(map[string]SQLExternalTable, len(cfg.ExternalTables)) if cfg.ExternalTables != nil { diff --git a/anysdk/operation_store.go b/anysdk/operation_store.go index 25e25a0..e35d33f 100644 --- a/anysdk/operation_store.go +++ b/anysdk/operation_store.go @@ -62,6 +62,7 @@ type OperationStore interface { GetGraphQL() GraphQL GetInverse() (OperationInverse, bool) GetStackQLConfig() StackQLConfig + GetQueryParamPushdown() (QueryParamPushdown, bool) GetParameters() map[string]Addressable GetPathItem() *openapi3.PathItem GetAPIMethod() string @@ -405,6 +406,42 @@ func (op *standardOpenAPIOperationStore) getStackQLConfig() (StackQLConfig, bool return rv, rv != nil } +// GetQueryParamPushdown returns the queryParamPushdown config with inheritance. +// It walks up the hierarchy: Method -> Resource -> Service -> ProviderService -> Provider +func (op *standardOpenAPIOperationStore) GetQueryParamPushdown() (QueryParamPushdown, bool) { + // Check method-level config first + if op.StackQLConfig != nil { + if qpp, ok := op.StackQLConfig.GetQueryParamPushdown(); ok { + return qpp, true + } + } + // Check resource-level config + if op.Resource != nil { + if qpp, ok := op.Resource.GetQueryParamPushdown(); ok { + return qpp, true + } + } + // Check service-level config + if op.OpenAPIService != nil { + if qpp, ok := op.OpenAPIService.getQueryParamPushdown(); ok { + return qpp, true + } + } + // Check providerService-level config + if op.ProviderService != nil { + if qpp, ok := op.ProviderService.GetQueryParamPushdown(); ok { + return qpp, true + } + } + // Check provider-level config + if op.Provider != nil { + if qpp, ok := op.Provider.GetQueryParamPushdown(); ok { + return qpp, true + } + } + return nil, false +} + func (op *standardOpenAPIOperationStore) GetAPIMethod() string { return op.APIMethod } diff --git a/anysdk/provider.go b/anysdk/provider.go index eeee5bd..7ef9062 100644 --- a/anysdk/provider.go +++ b/anysdk/provider.go @@ -28,6 +28,7 @@ type Provider interface { GetProviderServices() map[string]ProviderService GetPaginationRequestTokenSemantic() (TokenSemantic, bool) GetPaginationResponseTokenSemantic() (TokenSemantic, bool) + GetQueryParamPushdown() (QueryParamPushdown, bool) GetProviderService(key string) (ProviderService, error) getQueryTransposeAlgorithm() string GetRequestTranslateAlgorithm() string @@ -128,6 +129,13 @@ func (pr *standardProvider) GetPaginationResponseTokenSemantic() (TokenSemantic, return pr.StackQLConfig.Pagination.ResponseToken, true } +func (pr *standardProvider) GetQueryParamPushdown() (QueryParamPushdown, bool) { + if pr.StackQLConfig != nil { + return pr.StackQLConfig.GetQueryParamPushdown() + } + return nil, false +} + func (pr *standardProvider) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(pr) } diff --git a/anysdk/providerService.go b/anysdk/providerService.go index 999a512..3fa9c05 100644 --- a/anysdk/providerService.go +++ b/anysdk/providerService.go @@ -25,6 +25,7 @@ type ProviderService interface { GetResourcesShallow() (ResourceRegister, error) GetPaginationRequestTokenSemantic() (TokenSemantic, bool) getPaginationResponseTokenSemantic() (TokenSemantic, bool) + GetQueryParamPushdown() (QueryParamPushdown, bool) ConditionIsValid(lhs string, rhs interface{}) bool GetID() string GetServiceFragment(resourceKey string) (Service, error) @@ -178,6 +179,13 @@ func (sv *standardProviderService) getPaginationResponseTokenSemantic() (TokenSe return sv.StackQLConfig.Pagination.ResponseToken, true } +func (sv *standardProviderService) GetQueryParamPushdown() (QueryParamPushdown, bool) { + if sv.StackQLConfig != nil { + return sv.StackQLConfig.GetQueryParamPushdown() + } + return nil, false +} + func (sv *standardProviderService) ConditionIsValid(lhs string, rhs interface{}) bool { elem := sv.ToMap()[lhs] return reflect.TypeOf(elem) == reflect.TypeOf(rhs) diff --git a/anysdk/query_param_pushdown.go b/anysdk/query_param_pushdown.go new file mode 100644 index 0000000..c288412 --- /dev/null +++ b/anysdk/query_param_pushdown.go @@ -0,0 +1,351 @@ +package anysdk + +import ( + "fmt" + + "github.com/go-openapi/jsonpointer" +) + +// OData default values +const ( + ODataDialect = "odata" + CustomDialect = "custom" + DefaultSelectParamName = "$select" + DefaultSelectDelimiter = "," + DefaultFilterParamName = "$filter" + DefaultFilterSyntax = "odata" + DefaultOrderByParamName = "$orderby" + DefaultOrderBySyntax = "odata" + DefaultTopParamName = "$top" + DefaultCountParamName = "$count" + DefaultCountParamValue = "true" + DefaultCountResponseKey = "@odata.count" +) + +var ( + _ QueryParamPushdown = &standardQueryParamPushdown{} + _ jsonpointer.JSONPointable = standardQueryParamPushdown{} + _ SelectPushdown = &standardSelectPushdown{} + _ FilterPushdown = &standardFilterPushdown{} + _ OrderByPushdown = &standardOrderByPushdown{} + _ TopPushdown = &standardTopPushdown{} + _ CountPushdown = &standardCountPushdown{} +) + +// QueryParamPushdown represents the top-level configuration for query parameter pushdown +type QueryParamPushdown interface { + JSONLookup(token string) (interface{}, error) + GetSelect() (SelectPushdown, bool) + GetFilter() (FilterPushdown, bool) + GetOrderBy() (OrderByPushdown, bool) + GetTop() (TopPushdown, bool) + GetCount() (CountPushdown, bool) +} + +// SelectPushdown represents configuration for SELECT clause column projection pushdown +type SelectPushdown interface { + GetDialect() string + GetParamName() string + GetDelimiter() string + GetSupportedColumns() []string + IsColumnSupported(column string) bool +} + +// FilterPushdown represents configuration for WHERE clause filter pushdown +type FilterPushdown interface { + GetDialect() string + GetParamName() string + GetSyntax() string + GetSupportedOperators() []string + GetSupportedColumns() []string + IsOperatorSupported(operator string) bool + IsColumnSupported(column string) bool +} + +// OrderByPushdown represents configuration for ORDER BY clause pushdown +type OrderByPushdown interface { + GetDialect() string + GetParamName() string + GetSyntax() string + GetSupportedColumns() []string + IsColumnSupported(column string) bool +} + +// TopPushdown represents configuration for LIMIT clause pushdown +type TopPushdown interface { + GetDialect() string + GetParamName() string + GetMaxValue() int +} + +// CountPushdown represents configuration for SELECT COUNT(*) pushdown +type CountPushdown interface { + GetDialect() string + GetParamName() string + GetParamValue() string + GetResponseKey() string +} + +// standardQueryParamPushdown is the concrete implementation +type standardQueryParamPushdown struct { + Select *standardSelectPushdown `json:"select,omitempty" yaml:"select,omitempty"` + Filter *standardFilterPushdown `json:"filter,omitempty" yaml:"filter,omitempty"` + OrderBy *standardOrderByPushdown `json:"orderBy,omitempty" yaml:"orderBy,omitempty"` + Top *standardTopPushdown `json:"top,omitempty" yaml:"top,omitempty"` + Count *standardCountPushdown `json:"count,omitempty" yaml:"count,omitempty"` +} + +func (qpp standardQueryParamPushdown) JSONLookup(token string) (interface{}, error) { + switch token { + case "select": + return qpp.Select, nil + case "filter": + return qpp.Filter, nil + case "orderBy": + return qpp.OrderBy, nil + case "top": + return qpp.Top, nil + case "count": + return qpp.Count, nil + default: + return nil, fmt.Errorf("could not resolve token '%s' from QueryParamPushdown doc object", token) + } +} + +func (qpp *standardQueryParamPushdown) GetSelect() (SelectPushdown, bool) { + if qpp.Select == nil { + return nil, false + } + return qpp.Select, true +} + +func (qpp *standardQueryParamPushdown) GetFilter() (FilterPushdown, bool) { + if qpp.Filter == nil { + return nil, false + } + return qpp.Filter, true +} + +func (qpp *standardQueryParamPushdown) GetOrderBy() (OrderByPushdown, bool) { + if qpp.OrderBy == nil { + return nil, false + } + return qpp.OrderBy, true +} + +func (qpp *standardQueryParamPushdown) GetTop() (TopPushdown, bool) { + if qpp.Top == nil { + return nil, false + } + return qpp.Top, true +} + +func (qpp *standardQueryParamPushdown) GetCount() (CountPushdown, bool) { + if qpp.Count == nil { + return nil, false + } + return qpp.Count, true +} + +// standardSelectPushdown implements SelectPushdown +type standardSelectPushdown struct { + Dialect string `json:"dialect,omitempty" yaml:"dialect,omitempty"` + ParamName string `json:"paramName,omitempty" yaml:"paramName,omitempty"` + Delimiter string `json:"delimiter,omitempty" yaml:"delimiter,omitempty"` + SupportedColumns []string `json:"supportedColumns,omitempty" yaml:"supportedColumns,omitempty"` +} + +func (sp *standardSelectPushdown) GetDialect() string { + if sp.Dialect == "" { + return CustomDialect + } + return sp.Dialect +} + +func (sp *standardSelectPushdown) GetParamName() string { + if sp.ParamName == "" && sp.GetDialect() == ODataDialect { + return DefaultSelectParamName + } + return sp.ParamName +} + +func (sp *standardSelectPushdown) GetDelimiter() string { + if sp.Delimiter == "" { + if sp.GetDialect() == ODataDialect { + return DefaultSelectDelimiter + } + return "," + } + return sp.Delimiter +} + +func (sp *standardSelectPushdown) GetSupportedColumns() []string { + return sp.SupportedColumns +} + +func (sp *standardSelectPushdown) IsColumnSupported(column string) bool { + return isItemSupported(column, sp.SupportedColumns) +} + +// standardFilterPushdown implements FilterPushdown +type standardFilterPushdown struct { + Dialect string `json:"dialect,omitempty" yaml:"dialect,omitempty"` + ParamName string `json:"paramName,omitempty" yaml:"paramName,omitempty"` + Syntax string `json:"syntax,omitempty" yaml:"syntax,omitempty"` + SupportedOperators []string `json:"supportedOperators,omitempty" yaml:"supportedOperators,omitempty"` + SupportedColumns []string `json:"supportedColumns,omitempty" yaml:"supportedColumns,omitempty"` +} + +func (fp *standardFilterPushdown) GetDialect() string { + if fp.Dialect == "" { + return CustomDialect + } + return fp.Dialect +} + +func (fp *standardFilterPushdown) GetParamName() string { + if fp.ParamName == "" && fp.GetDialect() == ODataDialect { + return DefaultFilterParamName + } + return fp.ParamName +} + +func (fp *standardFilterPushdown) GetSyntax() string { + if fp.Syntax == "" && fp.GetDialect() == ODataDialect { + return DefaultFilterSyntax + } + return fp.Syntax +} + +func (fp *standardFilterPushdown) GetSupportedOperators() []string { + return fp.SupportedOperators +} + +func (fp *standardFilterPushdown) GetSupportedColumns() []string { + return fp.SupportedColumns +} + +func (fp *standardFilterPushdown) IsOperatorSupported(operator string) bool { + return isItemSupported(operator, fp.SupportedOperators) +} + +func (fp *standardFilterPushdown) IsColumnSupported(column string) bool { + return isItemSupported(column, fp.SupportedColumns) +} + +// standardOrderByPushdown implements OrderByPushdown +type standardOrderByPushdown struct { + Dialect string `json:"dialect,omitempty" yaml:"dialect,omitempty"` + ParamName string `json:"paramName,omitempty" yaml:"paramName,omitempty"` + Syntax string `json:"syntax,omitempty" yaml:"syntax,omitempty"` + SupportedColumns []string `json:"supportedColumns,omitempty" yaml:"supportedColumns,omitempty"` +} + +func (op *standardOrderByPushdown) GetDialect() string { + if op.Dialect == "" { + return CustomDialect + } + return op.Dialect +} + +func (op *standardOrderByPushdown) GetParamName() string { + if op.ParamName == "" && op.GetDialect() == ODataDialect { + return DefaultOrderByParamName + } + return op.ParamName +} + +func (op *standardOrderByPushdown) GetSyntax() string { + if op.Syntax == "" && op.GetDialect() == ODataDialect { + return DefaultOrderBySyntax + } + return op.Syntax +} + +func (op *standardOrderByPushdown) GetSupportedColumns() []string { + return op.SupportedColumns +} + +func (op *standardOrderByPushdown) IsColumnSupported(column string) bool { + return isItemSupported(column, op.SupportedColumns) +} + +// standardTopPushdown implements TopPushdown +type standardTopPushdown struct { + Dialect string `json:"dialect,omitempty" yaml:"dialect,omitempty"` + ParamName string `json:"paramName,omitempty" yaml:"paramName,omitempty"` + MaxValue int `json:"maxValue,omitempty" yaml:"maxValue,omitempty"` +} + +func (tp *standardTopPushdown) GetDialect() string { + if tp.Dialect == "" { + return CustomDialect + } + return tp.Dialect +} + +func (tp *standardTopPushdown) GetParamName() string { + if tp.ParamName == "" && tp.GetDialect() == ODataDialect { + return DefaultTopParamName + } + return tp.ParamName +} + +func (tp *standardTopPushdown) GetMaxValue() int { + return tp.MaxValue +} + +// standardCountPushdown implements CountPushdown +type standardCountPushdown struct { + Dialect string `json:"dialect,omitempty" yaml:"dialect,omitempty"` + ParamName string `json:"paramName,omitempty" yaml:"paramName,omitempty"` + ParamValue string `json:"paramValue,omitempty" yaml:"paramValue,omitempty"` + ResponseKey string `json:"responseKey,omitempty" yaml:"responseKey,omitempty"` +} + +func (cp *standardCountPushdown) GetDialect() string { + if cp.Dialect == "" { + return CustomDialect + } + return cp.Dialect +} + +func (cp *standardCountPushdown) GetParamName() string { + if cp.ParamName == "" && cp.GetDialect() == ODataDialect { + return DefaultCountParamName + } + return cp.ParamName +} + +func (cp *standardCountPushdown) GetParamValue() string { + if cp.ParamValue == "" && cp.GetDialect() == ODataDialect { + return DefaultCountParamValue + } + return cp.ParamValue +} + +func (cp *standardCountPushdown) GetResponseKey() string { + if cp.ResponseKey == "" && cp.GetDialect() == ODataDialect { + return DefaultCountResponseKey + } + return cp.ResponseKey +} + +// isItemSupported checks if an item is in the supported list +// Returns true if: list is nil/empty (all supported), list contains "*", or list contains the item +func isItemSupported(item string, supportedList []string) bool { + if len(supportedList) == 0 { + return true // empty/nil means all supported + } + for _, s := range supportedList { + if s == "*" || s == item { + return true + } + } + return false +} + +// GetTestingQueryParamPushdown returns a zero-value struct for testing +func GetTestingQueryParamPushdown() standardQueryParamPushdown { + return standardQueryParamPushdown{} +} diff --git a/anysdk/query_param_pushdown_test.go b/anysdk/query_param_pushdown_test.go new file mode 100644 index 0000000..1073066 --- /dev/null +++ b/anysdk/query_param_pushdown_test.go @@ -0,0 +1,295 @@ +package anysdk_test + +import ( + "testing" + + . "github.com/stackql/any-sdk/anysdk" + "gopkg.in/yaml.v3" + + "gotest.tools/assert" +) + +var ( + odataFullYamlInput string = ` +select: + dialect: odata + supportedColumns: + - "id" + - "displayName" + - "mail" +filter: + dialect: odata + supportedOperators: + - "eq" + - "ne" + - "gt" + - "lt" + - "contains" + - "startswith" + supportedColumns: + - "displayName" + - "status" + - "createdDate" +orderBy: + dialect: odata + supportedColumns: + - "name" + - "createdDate" +top: + dialect: odata + maxValue: 1000 +count: + dialect: odata +` + + customDialectYamlInput string = ` +select: + paramName: "fields" + delimiter: "," + supportedColumns: + - "*" +filter: + paramName: "filter" + syntax: "key_value" + supportedOperators: + - "eq" + supportedColumns: + - "status" + - "region" +orderBy: + paramName: "sort" + syntax: "prefix" + supportedColumns: + - "createdAt" + - "name" +top: + paramName: "limit" + maxValue: 100 +count: + paramName: "include_count" + paramValue: "1" + responseKey: "meta.total" +` + + minimalOdataYamlInput string = ` +filter: + dialect: odata + supportedOperators: + - "eq" +` +) + +func TestODataFullConfig(t *testing.T) { + qpp := GetTestingQueryParamPushdown() + err := yaml.Unmarshal([]byte(odataFullYamlInput), &qpp) + if err != nil { + t.Fatalf("TestODataFullConfig failed at unmarshal step, err = '%s'", err.Error()) + } + + // Test select pushdown + selectPD, ok := qpp.GetSelect() + if !ok { + t.Fatalf("TestODataFullConfig failed: expected select pushdown to exist") + } + assert.Equal(t, selectPD.GetDialect(), "odata") + assert.Equal(t, selectPD.GetParamName(), "$select") // OData default + assert.Equal(t, selectPD.GetDelimiter(), ",") // OData default + assert.Assert(t, selectPD.IsColumnSupported("displayName")) + assert.Assert(t, !selectPD.IsColumnSupported("unknownColumn")) + + // Test filter pushdown + filterPD, ok := qpp.GetFilter() + if !ok { + t.Fatalf("TestODataFullConfig failed: expected filter pushdown to exist") + } + assert.Equal(t, filterPD.GetDialect(), "odata") + assert.Equal(t, filterPD.GetParamName(), "$filter") // OData default + assert.Equal(t, filterPD.GetSyntax(), "odata") // OData default + assert.Assert(t, filterPD.IsOperatorSupported("eq")) + assert.Assert(t, filterPD.IsOperatorSupported("contains")) + assert.Assert(t, !filterPD.IsOperatorSupported("like")) + assert.Assert(t, filterPD.IsColumnSupported("displayName")) + assert.Assert(t, !filterPD.IsColumnSupported("unknownColumn")) + + // Test orderBy pushdown + orderByPD, ok := qpp.GetOrderBy() + if !ok { + t.Fatalf("TestODataFullConfig failed: expected orderBy pushdown to exist") + } + assert.Equal(t, orderByPD.GetDialect(), "odata") + assert.Equal(t, orderByPD.GetParamName(), "$orderby") // OData default + assert.Equal(t, orderByPD.GetSyntax(), "odata") // OData default + assert.Assert(t, orderByPD.IsColumnSupported("name")) + assert.Assert(t, !orderByPD.IsColumnSupported("unknownColumn")) + + // Test top pushdown + topPD, ok := qpp.GetTop() + if !ok { + t.Fatalf("TestODataFullConfig failed: expected top pushdown to exist") + } + assert.Equal(t, topPD.GetDialect(), "odata") + assert.Equal(t, topPD.GetParamName(), "$top") // OData default + assert.Equal(t, topPD.GetMaxValue(), 1000) + + // Test count pushdown + countPD, ok := qpp.GetCount() + if !ok { + t.Fatalf("TestODataFullConfig failed: expected count pushdown to exist") + } + assert.Equal(t, countPD.GetDialect(), "odata") + assert.Equal(t, countPD.GetParamName(), "$count") // OData default + assert.Equal(t, countPD.GetParamValue(), "true") // OData default + assert.Equal(t, countPD.GetResponseKey(), "@odata.count") // OData default + + t.Logf("TestODataFullConfig passed") +} + +func TestCustomDialectConfig(t *testing.T) { + qpp := GetTestingQueryParamPushdown() + err := yaml.Unmarshal([]byte(customDialectYamlInput), &qpp) + if err != nil { + t.Fatalf("TestCustomDialectConfig failed at unmarshal step, err = '%s'", err.Error()) + } + + // Test select pushdown with custom params + selectPD, ok := qpp.GetSelect() + if !ok { + t.Fatalf("TestCustomDialectConfig failed: expected select pushdown to exist") + } + assert.Equal(t, selectPD.GetDialect(), "custom") + assert.Equal(t, selectPD.GetParamName(), "fields") + assert.Equal(t, selectPD.GetDelimiter(), ",") + assert.Assert(t, selectPD.IsColumnSupported("anyColumn")) // "*" means all supported + + // Test filter pushdown with custom params + filterPD, ok := qpp.GetFilter() + if !ok { + t.Fatalf("TestCustomDialectConfig failed: expected filter pushdown to exist") + } + assert.Equal(t, filterPD.GetDialect(), "custom") + assert.Equal(t, filterPD.GetParamName(), "filter") + assert.Equal(t, filterPD.GetSyntax(), "key_value") + assert.Assert(t, filterPD.IsOperatorSupported("eq")) + assert.Assert(t, !filterPD.IsOperatorSupported("ne")) // Only eq is supported + + // Test orderBy pushdown with custom params + orderByPD, ok := qpp.GetOrderBy() + if !ok { + t.Fatalf("TestCustomDialectConfig failed: expected orderBy pushdown to exist") + } + assert.Equal(t, orderByPD.GetDialect(), "custom") + assert.Equal(t, orderByPD.GetParamName(), "sort") + assert.Equal(t, orderByPD.GetSyntax(), "prefix") + + // Test top pushdown with custom params + topPD, ok := qpp.GetTop() + if !ok { + t.Fatalf("TestCustomDialectConfig failed: expected top pushdown to exist") + } + assert.Equal(t, topPD.GetDialect(), "custom") + assert.Equal(t, topPD.GetParamName(), "limit") + assert.Equal(t, topPD.GetMaxValue(), 100) + + // Test count pushdown with custom params + countPD, ok := qpp.GetCount() + if !ok { + t.Fatalf("TestCustomDialectConfig failed: expected count pushdown to exist") + } + assert.Equal(t, countPD.GetDialect(), "custom") + assert.Equal(t, countPD.GetParamName(), "include_count") + assert.Equal(t, countPD.GetParamValue(), "1") + assert.Equal(t, countPD.GetResponseKey(), "meta.total") + + t.Logf("TestCustomDialectConfig passed") +} + +func TestMinimalODataConfig(t *testing.T) { + qpp := GetTestingQueryParamPushdown() + err := yaml.Unmarshal([]byte(minimalOdataYamlInput), &qpp) + if err != nil { + t.Fatalf("TestMinimalODataConfig failed at unmarshal step, err = '%s'", err.Error()) + } + + // Select should not exist + _, ok := qpp.GetSelect() + assert.Assert(t, !ok, "expected select pushdown to NOT exist") + + // Filter should exist with OData defaults + filterPD, ok := qpp.GetFilter() + if !ok { + t.Fatalf("TestMinimalODataConfig failed: expected filter pushdown to exist") + } + assert.Equal(t, filterPD.GetDialect(), "odata") + assert.Equal(t, filterPD.GetParamName(), "$filter") + assert.Assert(t, filterPD.IsOperatorSupported("eq")) + // All columns supported when not specified + assert.Assert(t, filterPD.IsColumnSupported("anyColumn")) + + // OrderBy should not exist + _, ok = qpp.GetOrderBy() + assert.Assert(t, !ok, "expected orderBy pushdown to NOT exist") + + // Top should not exist + _, ok = qpp.GetTop() + assert.Assert(t, !ok, "expected top pushdown to NOT exist") + + // Count should not exist + _, ok = qpp.GetCount() + assert.Assert(t, !ok, "expected count pushdown to NOT exist") + + t.Logf("TestMinimalODataConfig passed") +} + +func TestEmptySupportedColumns(t *testing.T) { + // When supportedColumns is empty/nil, all columns should be supported + qpp := GetTestingQueryParamPushdown() + yamlInput := ` +filter: + dialect: odata + supportedOperators: + - "eq" +` + err := yaml.Unmarshal([]byte(yamlInput), &qpp) + if err != nil { + t.Fatalf("TestEmptySupportedColumns failed at unmarshal step, err = '%s'", err.Error()) + } + + filterPD, ok := qpp.GetFilter() + if !ok { + t.Fatalf("TestEmptySupportedColumns failed: expected filter pushdown to exist") + } + + // All columns should be supported when list is empty + assert.Assert(t, filterPD.IsColumnSupported("anyColumn")) + assert.Assert(t, filterPD.IsColumnSupported("anotherColumn")) + + t.Logf("TestEmptySupportedColumns passed") +} + +func TestWildcardSupportedColumns(t *testing.T) { + // When supportedColumns contains "*", all columns should be supported + qpp := GetTestingQueryParamPushdown() + yamlInput := ` +filter: + dialect: odata + supportedOperators: + - "eq" + supportedColumns: + - "*" +` + err := yaml.Unmarshal([]byte(yamlInput), &qpp) + if err != nil { + t.Fatalf("TestWildcardSupportedColumns failed at unmarshal step, err = '%s'", err.Error()) + } + + filterPD, ok := qpp.GetFilter() + if !ok { + t.Fatalf("TestWildcardSupportedColumns failed: expected filter pushdown to exist") + } + + // All columns should be supported with "*" + assert.Assert(t, filterPD.IsColumnSupported("anyColumn")) + assert.Assert(t, filterPD.IsColumnSupported("anotherColumn")) + + t.Logf("TestWildcardSupportedColumns passed") +} diff --git a/anysdk/registry_test.go b/anysdk/registry_test.go index 0d092b4..59cbeed 100644 --- a/anysdk/registry_test.go +++ b/anysdk/registry_test.go @@ -647,3 +647,167 @@ func TestRegistryProviderLatestVersion(t *testing.T) { t.Logf("TestRegistryProviderLatestVersion passed\n") } + +func TestQueryParamPushdownConfig(t *testing.T) { + execLocalRegistryTestOnly(t, unsignedProvidersRegistryCfgStr, execTestQueryParamPushdownConfig) +} + +func execTestQueryParamPushdownConfig(t *testing.T, r RegistryAPI) { + // Test using OData TripPin reference service - supports full OData query capabilities + pr, err := r.LoadProviderByName("odata_trippin", "v00.00.00000") + if err != nil { + t.Fatalf("Test failed: %v", err) + } + + sh, err := pr.GetProviderService("main") + if err != nil { + t.Fatalf("Test failed: %v", err) + } + assert.Assert(t, sh != nil) + + // ===================================================== + // Test RESOURCE-LEVEL inheritance: 'people' resource + // Config is at resource level, overrides service-level + // ===================================================== + t.Log("Testing RESOURCE-level config inheritance (people)") + sv, err := r.GetServiceFragment(sh, "people") + assert.NilError(t, err) + assert.Assert(t, sv != nil) + + rsc, err := sv.GetResource("people") + assert.NilError(t, err) + + method, methodErr := rsc.GetMethods().FindMethod("list") + assert.NilError(t, methodErr) + assert.Assert(t, method != nil) + + // Get queryParamPushdown config using the inheritance-aware method + // This walks up: Method -> Resource -> Service -> ProviderService -> Provider + qpp, qppExists := method.GetQueryParamPushdown() + assert.Assert(t, qppExists, "expected queryParamPushdown config to exist") + + // Test filter config - full OData operator support (resource-level config) + filterPD, filterExists := qpp.GetFilter() + assert.Assert(t, filterExists, "expected filter pushdown config to exist") + assert.Equal(t, filterPD.GetDialect(), "odata") + assert.Equal(t, filterPD.GetParamName(), "$filter") // OData default + // Resource-level config has full operator support + assert.Assert(t, filterPD.IsOperatorSupported("eq")) + assert.Assert(t, filterPD.IsOperatorSupported("ne")) + assert.Assert(t, filterPD.IsOperatorSupported("gt"), "resource-level should have gt operator") + assert.Assert(t, filterPD.IsOperatorSupported("lt"), "resource-level should have lt operator") + assert.Assert(t, filterPD.IsOperatorSupported("contains")) + assert.Assert(t, filterPD.IsOperatorSupported("startswith")) + assert.Assert(t, filterPD.IsOperatorSupported("endswith")) + assert.Assert(t, filterPD.IsOperatorSupported("and")) + assert.Assert(t, filterPD.IsOperatorSupported("or")) + assert.Assert(t, !filterPD.IsOperatorSupported("like"), "OData doesn't support 'like'") + // No supportedColumns specified = all columns supported + assert.Assert(t, filterPD.IsColumnSupported("FirstName")) + assert.Assert(t, filterPD.IsColumnSupported("anyColumn")) + + // Test select config + selectPD, selectExists := qpp.GetSelect() + assert.Assert(t, selectExists, "expected select pushdown config to exist") + assert.Equal(t, selectPD.GetDialect(), "odata") + assert.Equal(t, selectPD.GetParamName(), "$select") // OData default + assert.Equal(t, selectPD.GetDelimiter(), ",") // OData default + // No supportedColumns = all columns supported + assert.Assert(t, selectPD.IsColumnSupported("FirstName")) + assert.Assert(t, selectPD.IsColumnSupported("anyColumn")) + + // Test orderBy config + orderByPD, orderByExists := qpp.GetOrderBy() + assert.Assert(t, orderByExists, "expected orderBy pushdown config to exist") + assert.Equal(t, orderByPD.GetDialect(), "odata") + assert.Equal(t, orderByPD.GetParamName(), "$orderby") // OData default + assert.Equal(t, orderByPD.GetSyntax(), "odata") // OData default + + // Test top config + topPD, topExists := qpp.GetTop() + assert.Assert(t, topExists, "expected top pushdown config to exist") + assert.Equal(t, topPD.GetDialect(), "odata") + assert.Equal(t, topPD.GetParamName(), "$top") // OData default + assert.Equal(t, topPD.GetMaxValue(), 0) // No maxValue set + + // Test count config + countPD, countExists := qpp.GetCount() + assert.Assert(t, countExists, "expected count pushdown config to exist") + assert.Equal(t, countPD.GetDialect(), "odata") + assert.Equal(t, countPD.GetParamName(), "$count") // OData default + assert.Equal(t, countPD.GetParamValue(), "true") // OData default + assert.Equal(t, countPD.GetResponseKey(), "@odata.count") // OData default + + // ===================================================== + // Test SERVICE-LEVEL inheritance: 'airlines' resource + // NO config at resource or method level - inherits from service + // ===================================================== + t.Log("Testing SERVICE-level config inheritance (airlines)") + svAirlines, err := r.GetServiceFragment(sh, "airlines") + assert.NilError(t, err) + + rscAirlines, err := svAirlines.GetResource("airlines") + assert.NilError(t, err) + + methodAirlines, methodAirlinesErr := rscAirlines.GetMethods().FindMethod("list") + assert.NilError(t, methodAirlinesErr) + + // Use inheritance-aware method - should get service-level config + qppAirlines, qppAirlinesExists := methodAirlines.GetQueryParamPushdown() + assert.Assert(t, qppAirlinesExists, "expected service-level queryParamPushdown to be inherited") + + // Service-level config only has eq and ne operators + filterAirlines, filterAirlinesExists := qppAirlines.GetFilter() + assert.Assert(t, filterAirlinesExists) + assert.Assert(t, filterAirlines.IsOperatorSupported("eq"), "service-level should have eq") + assert.Assert(t, filterAirlines.IsOperatorSupported("ne"), "service-level should have ne") + // Service-level does NOT have these operators (only resource-level people has them) + assert.Assert(t, !filterAirlines.IsOperatorSupported("gt"), "service-level should NOT have gt") + assert.Assert(t, !filterAirlines.IsOperatorSupported("contains"), "service-level should NOT have contains") + + // Service-level has select, orderBy, top, count + selectAirlines, selectAirlinesExists := qppAirlines.GetSelect() + assert.Assert(t, selectAirlinesExists, "service-level should have select config") + assert.Equal(t, selectAirlines.GetDialect(), "odata") + + // ===================================================== + // Test METHOD-LEVEL inheritance: 'airports' resource + // Config at method level overrides service-level + // ===================================================== + t.Log("Testing METHOD-level config inheritance (airports)") + svAirports, err := r.GetServiceFragment(sh, "airports") + assert.NilError(t, err) + + rscAirports, err := svAirports.GetResource("airports") + assert.NilError(t, err) + + methodAirports, methodAirportsErr := rscAirports.GetMethods().FindMethod("list") + assert.NilError(t, methodAirportsErr) + + // Use inheritance-aware method + qppAirports, qppAirportsExists := methodAirports.GetQueryParamPushdown() + assert.Assert(t, qppAirportsExists) + + // Method-level has restricted columns (not present in service-level) + filterAirports, filterAirportsExists := qppAirports.GetFilter() + assert.Assert(t, filterAirportsExists) + assert.Assert(t, filterAirports.IsColumnSupported("Name")) + assert.Assert(t, filterAirports.IsColumnSupported("IcaoCode")) + assert.Assert(t, !filterAirports.IsColumnSupported("Location"), "Location is not in supportedColumns") + // Method-level has contains operator (unlike service-level) + assert.Assert(t, filterAirports.IsOperatorSupported("contains"), "method-level should have contains") + + // Test select with restricted columns + selectAirports, selectAirportsExists := qppAirports.GetSelect() + assert.Assert(t, selectAirportsExists) + assert.Assert(t, selectAirports.IsColumnSupported("Name")) + assert.Assert(t, selectAirports.IsColumnSupported("IataCode")) + assert.Assert(t, !selectAirports.IsColumnSupported("Location")) + + // Test top with maxValue (method-level specific) + topAirports, topAirportsExists := qppAirports.GetTop() + assert.Assert(t, topAirportsExists) + assert.Equal(t, topAirports.GetMaxValue(), 100, "method-level should have maxValue=100") + + t.Logf("TestQueryParamPushdownConfig passed - all inheritance levels tested") +} diff --git a/anysdk/resource.go b/anysdk/resource.go index d940d1d..36f117b 100644 --- a/anysdk/resource.go +++ b/anysdk/resource.go @@ -27,6 +27,7 @@ type Resource interface { GetRequestTranslateAlgorithm() string GetPaginationRequestTokenSemantic() (TokenSemantic, bool) GetPaginationResponseTokenSemantic() (TokenSemantic, bool) + GetQueryParamPushdown() (QueryParamPushdown, bool) FindMethod(key string) (StandardOperationStore, error) GetFirstMethodFromSQLVerb(sqlVerb string) (StandardOperationStore, string, bool) GetFirstNamespaceMethodMatchFromSQLVerb(sqlVerb string, parameters map[string]interface{}) (StandardOperationStore, map[string]interface{}, bool) @@ -201,6 +202,13 @@ func (r *standardResource) GetPaginationResponseTokenSemantic() (TokenSemantic, return nil, false } +func (r *standardResource) GetQueryParamPushdown() (QueryParamPushdown, bool) { + if r.StackQLConfig != nil { + return r.StackQLConfig.GetQueryParamPushdown() + } + return nil, false +} + func (rsc standardResource) JSONLookup(token string) (interface{}, error) { ss := strings.Split(token, "/") tokenRoot := "" diff --git a/anysdk/service.go b/anysdk/service.go index 679a2b8..a0db3d9 100644 --- a/anysdk/service.go +++ b/anysdk/service.go @@ -34,6 +34,7 @@ type OpenAPIService interface { getPaginationRequestTokenSemantic() (TokenSemantic, bool) getPaginationResponseTokenSemantic() (TokenSemantic, bool) getQueryTransposeAlgorithm() string + getQueryParamPushdown() (QueryParamPushdown, bool) GetT() *openapi3.T getT() *openapi3.T iDiscoveryDoc() @@ -273,6 +274,13 @@ func (svc *standardService) getPaginationResponseTokenSemantic() (TokenSemantic, return nil, false } +func (svc *standardService) getQueryParamPushdown() (QueryParamPushdown, bool) { + if svc.StackQLConfig != nil { + return svc.StackQLConfig.GetQueryParamPushdown() + } + return nil, false +} + func (svc *standardService) GetSchemas() (map[string]Schema, error) { rv := make(map[string]Schema) for k, sv := range svc.Components.Schemas { diff --git a/cicd/tools/api/exported_funcs.txt b/cicd/tools/api/exported_funcs.txt index 5e8a8cc..7b42784 100644 --- a/cicd/tools/api/exported_funcs.txt +++ b/cicd/tools/api/exported_funcs.txt @@ -49,6 +49,7 @@ func GetServicesHeader func GetSubObjFromNode func GetSubObjTyped func GetTabulation +func GetTestingQueryParamPushdown func GetTestingView func GetTxnCoordinatorCfgCfg func InterfaceToBytes @@ -183,9 +184,11 @@ func TestBasicAddressSpaceAWSCurrent func TestBasicAddressSpaceGoogleCurrent func TestBestEffortXMLStreamTransform func TestConfigDrivenAliasedAddressSpaceGoogleCurrent +func TestCustomDialectConfig func TestDeepDiscoveryGoogleCurrent func TestDiscovery01 func TestDiscoveryAWS +func TestEmptySupportedColumns func TestFatConfigDrivenAliasedAddressSpaceGoogleCurrent func TestFragmentedResourcesFile func TestGet @@ -210,10 +213,12 @@ func TestLocalTemplatedCompositeServiceFile func TestLocalValidateAndParseValidProviderFile func TestMeaningfulStreamTransform func TestMeaningfulXMLStreamTransform +func TestMinimalODataConfig func TestMethodLevelVariableHostRoutingFutureProofed func TestMonolithicCompositeServiceFile func TestNewAddressSpace func TestNoFallbackViewApi +func TestODataFullConfig func TestOpensslCertTextStreamTransform func TestPersistence01 func TestPersistenceSetup @@ -265,10 +270,12 @@ func TestValidateAndParseGoogleProviderFile func TestValidateAndParse_ValidJSON func TestVariableHostRouting func TestVariableHostRoutingFutureProofed +func TestWildcardSupportedColumns func TestXMLMArshal func TestXMLRequestBody func TestXMLSchemaInterrogation func TestXPathHandle +func TestQueryParamPushdownConfig func TranslateServiceKeyGenericProviderToIql func TrimSelectItemsKey func ValidateAndParse diff --git a/cicd/tools/api/exported_interfaces.txt b/cicd/tools/api/exported_interfaces.txt index fdcf15a..ae7e158 100644 --- a/cicd/tools/api/exported_interfaces.txt +++ b/cicd/tools/api/exported_interfaces.txt @@ -28,6 +28,7 @@ type ClientConfiguratorInput interface type Column interface type ColumnDescriptor interface type ControlAttributes interface +type CountPushdown interface type DataFlowCfg interface type DiscoveryDoc interface type ExecContext interface @@ -37,6 +38,7 @@ type Executor interface type ExpectedRequest interface type ExpectedResponse interface type FileValidator interface +type FilterPushdown interface type GQLReader interface type GraphQL interface type HTTPArmoury interface @@ -67,6 +69,7 @@ type ObjectWithLineage interface type ObjectWithLineageCollection interface type ObjectWithLineageCollectionConfig interface type ObjectWithoutLineage interface +type OrderByPushdown interface type OpenAPIService interface type OperationInverse interface type OperationSelector interface @@ -87,6 +90,7 @@ type Provider interface type ProviderService interface type ProviderServiceResourceAnalyzer interface type QueryElement interface +type QueryParamPushdown interface type QueryTransposer interface type QueryVar interface type RDBMSSystem interface @@ -103,6 +107,7 @@ type SQLEngine interface type SQLExternalColumn interface type SQLExternalConnection interface type SQLExternalTable interface +type SelectPushdown interface type Schema interface type Service interface type SessionContext interface @@ -118,6 +123,7 @@ type StreamTransformer interface type StreamTransformerFactory interface type Tabulation interface type TokenSemantic interface +type TopPushdown interface type Transform interface type TransformerLocator interface type Transport interface diff --git a/docs/future_api.md b/docs/future_api.md index a3d9f66..a640b04 100644 --- a/docs/future_api.md +++ b/docs/future_api.md @@ -77,3 +77,28 @@ New exported API is allowed only when: 1. Update the API snapshot files. 2. Update this document to justify the addition. +## 5. Approved API Additions + +### 5.1 Query Parameter Pushdown (added 2025-11) + +**Purpose:** Enable SQL clause pushdown to API query parameters for OData and custom APIs. + +**New Interfaces (in `anysdk` package):** + +| Interface | Description | +|-----------|-------------| +| `QueryParamPushdown` | Root interface for accessing pushdown configuration | +| `SelectPushdown` | Column projection (`$select`) configuration | +| `FilterPushdown` | Row filtering (`$filter`) with operator/column restrictions | +| `OrderByPushdown` | Ordering (`$orderby`) configuration | +| `TopPushdown` | Row limit (`$top`) with optional max value | +| `CountPushdown` | Count support (`$count`) configuration | + +**New Functions:** + +| Function | Description | +|----------|-------------| +| `GetTestingQueryParamPushdown` | Test helper for pushdown config validation | + +**Justification:** These interfaces extend `StackQLConfig` to support predicate pushdown, enabling efficient SQL-to-API translation for OData-compatible endpoints. They fit the Runtime Profile as configuration extensions for provider operations. + diff --git a/docs/provider_spec.md b/docs/provider_spec.md new file mode 100644 index 0000000..fa916e6 --- /dev/null +++ b/docs/provider_spec.md @@ -0,0 +1,799 @@ +# StackQL Provider Specification + +This document provides a comprehensive specification for StackQL providers, which are OpenAPI 3.0 documents augmented with custom `x-stackQL-*` extensions that enable SQL-like semantics for REST APIs. + +## Table of Contents + +1. [Overview](#overview) +2. [Provider Hierarchy](#provider-hierarchy) +3. [Provider Document Structure](#provider-document-structure) +4. [Service Document Structure](#service-document-structure) +5. [Resource Definition](#resource-definition) +6. [Method Definition](#method-definition) +7. [SQL Verb Mapping](#sql-verb-mapping) +8. [Configuration Options](#configuration-options) +9. [Configuration Inheritance](#configuration-inheritance) +10. [OpenAPI Extensions Reference](#openapi-extensions-reference) +11. [Response Processing](#response-processing) +12. [Authentication](#authentication) +13. [Pagination](#pagination) + +--- + +## Overview + +StackQL providers transform REST APIs into SQL-queryable resources. A provider consists of: + +- **Provider Document** (`provider.yaml`): Root configuration containing provider metadata and service references +- **Service Documents**: OpenAPI 3.0 specifications with StackQL extensions defining resources and methods +- **Resources**: Logical groupings of related API operations mapped to SQL tables +- **Methods**: Individual API operations mapped to SQL verbs (SELECT, INSERT, UPDATE, DELETE) + +--- + +## Provider Hierarchy + +``` +Provider +├── config (provider-level) +└── ProviderServices + ├── config (service-level) + └── Service (OpenAPI document) + └── x-stackQL-resources + └── Resource + ├── config (resource-level) + └── Methods + └── Method + └── config (method-level) +``` + +Configuration cascades from higher levels to lower levels, with lower-level config overriding higher-level config for most options. See [Configuration Inheritance](#configuration-inheritance) for details. + +--- + +## Provider Document Structure + +The provider document (`provider.yaml`) is the entry point for a StackQL provider. + +### Required Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique provider identifier | +| `name` | string | Provider name (e.g., "google", "aws", "azure") | +| `title` | string | Human-readable title | +| `version` | string | Provider version | +| `providerServices` | map | Map of service names to service definitions | + +### Optional Fields + +| Field | Type | Description | +|-------|------|-------------| +| `description` | string | Provider description | +| `protocolType` | string | Protocol type: `"http"` (default) or `"local_templated"` | +| `config` | object | Provider-level configuration | +| `responseKeys` | object | Default response extraction keys | + +### Example + +```yaml +id: example +name: example +title: Example Provider +version: v1.0.0 +description: Example StackQL provider +protocolType: http + +providerServices: + api: + id: example.api + name: api + title: Example API + version: v1 + preferred: true + service: + $ref: services/api.yaml + resources: + $ref: resources/api-resources.yaml + +config: + auth: + type: api_key + name: X-API-Key + location: header + api_key_var: EXAMPLE_API_KEY + pagination: + requestToken: + key: page + location: query + responseToken: + key: nextPage + location: body + +responseKeys: + selectItemsKey: "items" + deleteItemsKey: "id" +``` + +--- + +## Service Document Structure + +Service documents are OpenAPI 3.0 specifications with StackQL extensions. + +### Structure + +```yaml +openapi: 3.0.0 +info: + title: Service Title + version: "1.0.0" + +servers: + - url: https://api.example.com + variables: + region: + default: us-east-1 + enum: [us-east-1, us-west-2] + +components: + x-stackQL-resources: + # Resource definitions (see Resource Definition section) + + schemas: + # OpenAPI schemas + + parameters: + # Reusable parameters + + securitySchemes: + # Authentication schemes + +paths: + # OpenAPI path definitions + +x-stackQL-config: + # Service-level configuration (optional) +``` + +--- + +## Resource Definition + +Resources are defined within `components.x-stackQL-resources` and represent SQL-queryable tables. + +### Required Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Fully qualified resource ID (e.g., `provider.service.resource`) | +| `name` | string | Resource name (typically snake_case, plural) | +| `methods` | map | Map of method names to method definitions | +| `sqlVerbs` | object | SQL verb mappings | + +### Optional Fields + +| Field | Type | Description | +|-------|------|-------------| +| `title` | string | Human-readable title | +| `description` | string | Resource description | +| `config` | object | Resource-level configuration | +| `serviceDoc` | object | Reference to service document | +| `selectorAlgorithm` | string | Method selection algorithm | +| `baseUrl` | string | Base URL override | + +### Example + +```yaml +components: + x-stackQL-resources: + instances: + id: google.compute.instances + name: instances + title: Compute Instances + description: Virtual machine instances + + methods: + list: + operation: + $ref: '#/paths/~1projects~1{project}~1zones~1{zone}~1instances/get' + response: + mediaType: application/json + openAPIDocKey: '200' + objectKey: $.items + get: + operation: + $ref: '#/paths/~1projects~1{project}~1zones~1{zone}~1instances~1{instance}/get' + response: + mediaType: application/json + openAPIDocKey: '200' + insert: + operation: + $ref: '#/paths/~1projects~1{project}~1zones~1{zone}~1instances/post' + request: + mediaType: application/json + response: + mediaType: application/json + openAPIDocKey: '200' + delete: + operation: + $ref: '#/paths/~1projects~1{project}~1zones~1{zone}~1instances~1{instance}/delete' + response: + mediaType: application/json + openAPIDocKey: '200' + + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/instances/methods/list' + - $ref: '#/components/x-stackQL-resources/instances/methods/get' + insert: + - $ref: '#/components/x-stackQL-resources/instances/methods/insert' + update: [] + replace: [] + delete: + - $ref: '#/components/x-stackQL-resources/instances/methods/delete' +``` + +--- + +## Method Definition + +Methods define individual API operations and how they map to SQL semantics. + +### Required Fields + +| Field | Type | Description | +|-------|------|-------------| +| `operation` | object | Reference to OpenAPI operation (`$ref`) | + +### Optional Fields + +| Field | Type | Description | +|-------|------|-------------| +| `response` | object | Response configuration | +| `request` | object | Request configuration | +| `config` | object | Method-level configuration | +| `servers` | array | Operation-specific servers | +| `inverse` | object | Rollback operation definition | +| `apiMethod` | string | HTTP method override | +| `serviceName` | string | Service name override | + +### Response Configuration + +```yaml +response: + mediaType: application/json # Response content type + openAPIDocKey: '200' # Response code to use for schema + objectKey: $.items # JSONPath to extract items + overrideMediaType: application/json # Override response parsing + schema_override: # Custom response schema + $ref: '#/components/schemas/CustomSchema' + async_schema_override: # Async operation schema + $ref: '#/components/schemas/AsyncSchema' + asyncOverrideMediaType: application/json + projection_map: # Response field projections + alias: actualFieldName + transform: # Response transformation + type: golang_template_mxj_v0.1.0 + body: | + {"processed": {{.field}}} +``` + +### Request Configuration + +```yaml +request: + mediaType: application/json # Request content type + default: '{"key": "default_value"}' # Default request body + base: '{"always": "included"}' # Base request body (always merged) + required: [field1, field2] # Required body fields + projection_map: # Request field projections + alias: actualFieldName + xmlDeclaration: '' + xmlTransform: unescape + xmlRootAnnotation: '' +``` + +### Complete Example + +```yaml +methods: + create: + operation: + $ref: '#/paths/~1resources/post' + + request: + mediaType: application/json + required: [name, type] + + response: + mediaType: application/json + openAPIDocKey: '201' + + config: + queryParamPushdown: + filter: + dialect: odata + supportedOperators: ["eq", "ne"] + + inverse: + sqlVerb: + $ref: '#/components/x-stackQL-resources/resources/methods/delete' + tokens: + resourceId: + key: $.id + location: body + algorithm: jsonpath +``` + +--- + +## SQL Verb Mapping + +SQL verbs map SQL query types to API methods. + +### Supported Verbs + +| SQL Verb | SQL Statement | Typical HTTP Method | +|----------|--------------|---------------------| +| `select` | SELECT | GET | +| `insert` | INSERT | POST | +| `update` | UPDATE | PATCH | +| `replace` | REPLACE (full update) | PUT | +| `delete` | DELETE | DELETE | +| `exec` | EXEC (procedures) | Any | + +### Method Selection Algorithm + +When executing a SQL query, StackQL selects the appropriate method based on: + +1. **SQL Verb Match**: Match query type to `sqlVerbs` mapping +2. **Parameter Match**: Find methods where provided parameters satisfy required parameters +3. **Selectivity Order**: Methods are tried in order listed; place less selective methods (fewer required params) first + +### Overloading Example + +```yaml +sqlVerbs: + select: + # List method - no required path params, matches broad queries + - $ref: '#/components/x-stackQL-resources/instances/methods/list' + # Get method - requires instance ID, matches specific queries + - $ref: '#/components/x-stackQL-resources/instances/methods/get' +``` + +Query behavior: +- `SELECT * FROM instances WHERE project='p' AND zone='z'` → Uses `list` method +- `SELECT * FROM instances WHERE project='p' AND zone='z' AND instance='i'` → Uses `get` method + +--- + +## Configuration Options + +Configuration can be set at provider, providerService, resource, or method levels. + +### Available Config Options + +| Option | Description | Inherits? | +|--------|-------------|-----------| +| `auth` | Authentication configuration | Yes | +| `pagination` | Pagination token handling | Yes | +| `queryParamTranspose` | Query parameter transformation | Yes | +| `requestTranslate` | Request transformation | Yes | +| `requestBodyTranslate` | Request body transformation | Yes | +| `variations` | Schema variation handling | Yes | +| `views` | SQL view definitions | No | +| `sqlExternalTables` | External table definitions | No | +| `queryParamPushdown` | Query pushdown to API params | Yes | + +### Config Structure + +```yaml +config: + auth: + type: api_key + name: X-API-Key + location: header + + pagination: + requestToken: + key: pageToken + location: query + responseToken: + key: nextPageToken + location: body + + queryParamTranspose: + algorithm: default + + requestTranslate: + algorithm: default + + requestBodyTranslate: + algorithm: naive + + variations: + isObjectSchemaImplicitlyUnioned: false + + views: + select: + predicate: 'sqlDialect == "stackql"' + ddl: | + SELECT id, name FROM resource + + sqlExternalTables: + external_data: + catalogName: external + schemaName: public + name: data + columns: + - name: id + type: string + + queryParamPushdown: + filter: + dialect: odata + supportedOperators: ["eq", "ne"] +``` + +--- + +## Configuration Inheritance + +Most configuration options inherit from higher levels in the hierarchy: + +``` +Method → Resource → Service → ProviderService → Provider +``` + +**With Inheritance** (lower level overrides higher level): +- `auth` +- `pagination` +- `queryParamTranspose` +- `requestTranslate` +- `requestBodyTranslate` +- `variations` +- `queryParamPushdown` + +**Without Inheritance** (must be set at specific level): +- `views` - Set at method or resource level +- `sqlExternalTables` - Set at method or resource level + +### Query Parameter Pushdown Inheritance + +The `queryParamPushdown` configuration supports full inheritance. Set a default at service level and override at resource or method level as needed: + +```yaml +# Service-level default - inherited by all resources/methods +x-stackQL-config: + queryParamPushdown: + select: + dialect: odata + filter: + dialect: odata + supportedOperators: ["eq", "ne"] + orderBy: + dialect: odata + top: + dialect: odata + count: + dialect: odata + +components: + x-stackQL-resources: + # Inherits service-level config (no config specified) + airlines: + id: provider.service.airlines + name: airlines + methods: + list: + operation: + $ref: '#/paths/~1airlines/get' + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/airlines/methods/list' + + # Resource-level override (more operators) + people: + id: provider.service.people + name: people + config: + queryParamPushdown: + filter: + dialect: odata + supportedOperators: ["eq", "ne", "gt", "lt", "contains"] + methods: + list: + operation: + $ref: '#/paths/~1people/get' + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/people/methods/list' + + # Method-level override (restricted columns) + airports: + id: provider.service.airports + name: airports + methods: + list: + operation: + $ref: '#/paths/~1airports/get' + config: + queryParamPushdown: + select: + dialect: odata + supportedColumns: ["Name", "IcaoCode"] + filter: + dialect: odata + supportedColumns: ["Name", "IcaoCode"] + top: + maxValue: 100 + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/airports/methods/list' +``` + +--- + +## OpenAPI Extensions Reference + +### Document-Level Extensions + +| Extension | Location | Description | +|-----------|----------|-------------| +| `x-stackQL-resources` | `components` | Resource definitions | +| `x-stackQL-config` | Root or `components` | Service-level configuration | +| `x-stackQL-provider` | `info` | Embedded provider metadata | + +### Operation-Level Extensions + +| Extension | Description | +|-----------|-------------| +| `x-stackQL-resource` | Associates operation with a resource | +| `x-stackQL-method` | Specifies method name | +| `x-stackQL-verb` | Maps to SQL verb | +| `x-stackQL-objectKey` | JSONPath for response extraction | +| `x-stackQL-graphQL` | GraphQL operation configuration | + +### Schema-Level Extensions + +| Extension | Description | +|-----------|-------------| +| `x-stackQL-stringOnly` | Serialize as string regardless of type | +| `x-stackQL-alias` | Property aliasing | +| `x-alwaysRequired` | Mark parameter as always required | + +--- + +## Response Processing + +### Object Key Extraction + +The `objectKey` field specifies how to extract items from API responses. + +**JSONPath Examples:** +```yaml +objectKey: $.items # Array at 'items' key +objectKey: $.data.results # Nested path +objectKey: $[*] # Root array +objectKey: $.value[*] # OData-style response +``` + +**XPath Examples (for XML):** +```yaml +objectKey: /Response/Items/Item +objectKey: //Volume +``` + +### Response Transformation + +Transform responses using Go templates: + +```yaml +response: + transform: + type: golang_template_mxj_v0.1.0 + body: | + { + "items": [ + {{- range $i, $item := .data.items }} + {{- if $i}},{{end}} + {"id": {{printf "%q" $item.id}}, "name": {{printf "%q" $item.name}}} + {{- end }} + ] + } +``` + +--- + +## Authentication + +### Supported Auth Types + +| Type | Description | +|------|-------------| +| `api_key` | API key in header or query | +| `basic` | HTTP Basic authentication | +| `bearer` | Bearer token | +| `service_account` | Google-style service account | +| `oauth2` | OAuth 2.0 client credentials | +| `aws_signing_v4` | AWS Signature Version 4 | +| `azure_default` | Azure default credentials | +| `custom` | Custom authentication | + +### Example Configurations + +**API Key:** +```yaml +auth: + type: api_key + name: X-API-Key + location: header # or "query" + api_key_var: MY_API_KEY # Environment variable +``` + +**OAuth2 Client Credentials:** +```yaml +auth: + type: oauth2 + grant_type: client_credentials + token_url: https://auth.example.com/token + client_id_env_var: CLIENT_ID + client_secret_env_var: CLIENT_SECRET + scopes: + - read + - write +``` + +**Service Account (Google):** +```yaml +auth: + type: service_account + credentialsenvvar: GOOGLE_CREDENTIALS + scopes: + - https://www.googleapis.com/auth/cloud-platform +``` + +--- + +## Pagination + +### Token-Based Pagination + +```yaml +pagination: + requestToken: + key: pageToken # Request parameter name + location: query # "query", "header", or "body" + responseToken: + key: nextPageToken # Response field with next token + location: body # "body" or "header" +``` + +### Offset-Based Pagination + +```yaml +pagination: + requestToken: + key: offset + location: query + responseToken: + key: next_offset + location: body +``` + +### Link Header Pagination (GitHub-style) + +```yaml +pagination: + requestToken: + key: page + location: query + responseToken: + key: Link + location: header + algorithm: link_header_next +``` + +--- + +## Query Parameter Pushdown + +Enables SQL clause pushdown to API query parameters for filtering, projection, ordering, and limiting. + +### Supported Pushdown Types + +| Type | SQL Clause | OData Parameter | Description | +|------|------------|-----------------|-------------| +| `filter` | WHERE | `$filter` | Row filtering | +| `select` | SELECT columns | `$select` | Column projection | +| `orderBy` | ORDER BY | `$orderby` | Result ordering | +| `top` | LIMIT | `$top` | Row limiting | +| `count` | COUNT(*) | `$count` | Total count | + +### OData Configuration + +```yaml +config: + queryParamPushdown: + select: + dialect: odata + filter: + dialect: odata + supportedOperators: + - "eq" + - "ne" + - "gt" + - "lt" + - "ge" + - "le" + - "contains" + - "startswith" + - "endswith" + orderBy: + dialect: odata + top: + dialect: odata + count: + dialect: odata +``` + +### Custom API Configuration + +```yaml +config: + queryParamPushdown: + select: + paramName: "fields" + delimiter: "," + filter: + paramName: "filter" + syntax: "key_value" + supportedOperators: ["eq"] + supportedColumns: ["status", "region"] + orderBy: + paramName: "sort" + syntax: "prefix" + supportedColumns: ["createdAt", "name"] + top: + paramName: "limit" + maxValue: 100 +``` + +### Supported Filter Syntaxes + +| Syntax | Example Output | +|--------|---------------| +| `odata` | `$filter=status eq 'active'` | +| `key_value` | `filter[status]=active` | +| `simple` | `status=active` | + +### Supported OrderBy Syntaxes + +| Syntax | Example | +|--------|---------| +| `odata` | `$orderby=name desc` | +| `prefix` | `sort=-name` | +| `suffix` | `sort=name:desc` | + +--- + +## Best Practices + +1. **Resource Naming**: Use plural, snake_case names (e.g., `instances`, `storage_accounts`) + +2. **Method Naming**: Use descriptive names matching the operation (`list`, `get`, `create`, `delete`) + +3. **SQL Verb Ordering**: Order methods by ascending parameter count for correct selection + +4. **Object Keys**: Use JSONPath for consistent response extraction + +5. **Schema Reuse**: Define schemas in `components/schemas` and reference them + +6. **Configuration Placement**: + - Place common config (auth, pagination) at provider or service level + - Place `queryParamPushdown` at method level + +7. **Parameter Validation**: Use OpenAPI schema validation for parameters + +8. **Documentation**: Include descriptions for resources, methods, and parameters diff --git a/test/registry/unsigned-src/odata_trippin/v00.00.00000/provider.yaml b/test/registry/unsigned-src/odata_trippin/v00.00.00000/provider.yaml new file mode 100644 index 0000000..1eaa776 --- /dev/null +++ b/test/registry/unsigned-src/odata_trippin/v00.00.00000/provider.yaml @@ -0,0 +1,16 @@ +id: odata_trippin +name: odata_trippin +title: OData TripPin Reference Service +version: v00.00.00000 +description: OData TripPin reference service - demonstrates full OData query support +openapi: 3.0.0 +providerServices: + main: + id: main:v00.00.00000 + name: main + title: TripPin Service + version: v00.00.00000 + description: OData TripPin reference service with full query parameter pushdown support + preferred: true + service: + $ref: odata_trippin/v00.00.00000/services/main.yaml diff --git a/test/registry/unsigned-src/odata_trippin/v00.00.00000/services/main.yaml b/test/registry/unsigned-src/odata_trippin/v00.00.00000/services/main.yaml new file mode 100644 index 0000000..32a6728 --- /dev/null +++ b/test/registry/unsigned-src/odata_trippin/v00.00.00000/services/main.yaml @@ -0,0 +1,381 @@ +openapi: 3.0.0 +info: + title: OData TripPin Reference Service + version: v00.00.00000 + description: | + The TripPin service is the official OData reference implementation. + It demonstrates full OData query capabilities including $filter, $select, + $orderby, $top, $skip, and $count. + + This provider demonstrates queryParamPushdown config inheritance: + - Service-level: Basic OData config (inherited by airlines) + - Resource-level: Full OData config for people (overrides service) + - Method-level: Restricted config for airports (overrides service) +servers: + - url: https://services.odata.org/TripPinRESTierService + description: OData TripPin Reference Service + +# Service-level queryParamPushdown config - inherited by all resources/methods +# unless overridden at resource or method level +x-stackQL-config: + queryParamPushdown: + select: + dialect: odata + filter: + dialect: odata + supportedOperators: + - "eq" + - "ne" + orderBy: + dialect: odata + top: + dialect: odata + count: + dialect: odata + +paths: + /People: + get: + operationId: people_list + summary: List people + description: Retrieves a list of people with full OData query support + parameters: + - name: $filter + in: query + description: OData filter expression + schema: + type: string + - name: $select + in: query + description: Comma-separated list of properties to include + schema: + type: string + - name: $orderby + in: query + description: Order results by property (e.g., "FirstName desc") + schema: + type: string + - name: $top + in: query + description: Maximum number of results to return + schema: + type: integer + - name: $count + in: query + description: Include count of matching resources + schema: + type: boolean + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/PeopleResponse' + /Airlines: + get: + operationId: airlines_list + summary: List airlines + parameters: + - name: $filter + in: query + schema: + type: string + - name: $select + in: query + schema: + type: string + - name: $orderby + in: query + schema: + type: string + - name: $top + in: query + schema: + type: integer + - name: $count + in: query + schema: + type: boolean + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/AirlinesResponse' + /Airports: + get: + operationId: airports_list + summary: List airports + parameters: + - name: $filter + in: query + schema: + type: string + - name: $select + in: query + schema: + type: string + - name: $orderby + in: query + schema: + type: string + - name: $top + in: query + schema: + type: integer + - name: $count + in: query + schema: + type: boolean + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/AirportsResponse' +components: + schemas: + PeopleResponse: + type: object + properties: + '@odata.context': + type: string + '@odata.count': + type: integer + value: + type: array + items: + $ref: '#/components/schemas/Person' + Person: + type: object + properties: + UserName: + type: string + FirstName: + type: string + LastName: + type: string + MiddleName: + type: string + Gender: + type: string + enum: [Male, Female, Unknown] + Age: + type: integer + Emails: + type: array + items: + type: string + FavoriteFeature: + type: string + Features: + type: array + items: + type: string + AddressInfo: + type: array + items: + $ref: '#/components/schemas/Location' + HomeAddress: + $ref: '#/components/schemas/Location' + Location: + type: object + properties: + Address: + type: string + City: + $ref: '#/components/schemas/City' + City: + type: object + properties: + Name: + type: string + CountryRegion: + type: string + Region: + type: string + AirlinesResponse: + type: object + properties: + '@odata.context': + type: string + '@odata.count': + type: integer + value: + type: array + items: + $ref: '#/components/schemas/Airline' + Airline: + type: object + properties: + AirlineCode: + type: string + Name: + type: string + AirportsResponse: + type: object + properties: + '@odata.context': + type: string + '@odata.count': + type: integer + value: + type: array + items: + $ref: '#/components/schemas/Airport' + Airport: + type: object + properties: + IcaoCode: + type: string + Name: + type: string + IataCode: + type: string + Location: + $ref: '#/components/schemas/AirportLocation' + AirportLocation: + type: object + properties: + Address: + type: string + City: + $ref: '#/components/schemas/City' + Loc: + type: object + properties: + type: + type: string + coordinates: + type: array + items: + type: number + x-stackQL-resources: + # RESOURCE-LEVEL CONFIG: people has config at resource level + # This overrides the service-level config with full OData support + people: + id: odata_trippin.main.people + name: people + title: People + description: People in the TripPin service - demonstrates RESOURCE-level config inheritance + config: + queryParamPushdown: + # Full OData support - overrides service-level config + select: + dialect: odata + filter: + dialect: odata + supportedOperators: + - "eq" + - "ne" + - "gt" + - "ge" + - "lt" + - "le" + - "and" + - "or" + - "not" + - "contains" + - "startswith" + - "endswith" + orderBy: + dialect: odata + top: + dialect: odata + count: + dialect: odata + methods: + list: + operation: + $ref: '#/paths/~1People/get' + response: + mediaType: application/json + openAPIDocKey: '200' + objectKey: $.value[*] + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/people/methods/list' + insert: [] + update: [] + replace: [] + delete: [] + + # SERVICE-LEVEL INHERITANCE: airlines has NO config at resource or method level + # It inherits queryParamPushdown from the service-level x-stackQL-config + airlines: + id: odata_trippin.main.airlines + name: airlines + title: Airlines + description: Airlines in the TripPin service - demonstrates SERVICE-level config inheritance (no resource or method config) + methods: + list: + operation: + $ref: '#/paths/~1Airlines/get' + response: + mediaType: application/json + openAPIDocKey: '200' + objectKey: $.value[*] + # NO config here - inherits from service-level x-stackQL-config + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/airlines/methods/list' + insert: [] + update: [] + replace: [] + delete: [] + + # METHOD-LEVEL CONFIG: airports has config at method level + # This overrides the service-level config with restricted column support + airports: + id: odata_trippin.main.airports + name: airports + title: Airports + description: Airports in the TripPin service - demonstrates METHOD-level config (overrides service-level) + methods: + list: + operation: + $ref: '#/paths/~1Airports/get' + response: + mediaType: application/json + openAPIDocKey: '200' + objectKey: $.value[*] + config: + queryParamPushdown: + select: + dialect: odata + supportedColumns: + - "IcaoCode" + - "Name" + - "IataCode" + filter: + dialect: odata + supportedOperators: + - "eq" + - "ne" + - "contains" + - "startswith" + supportedColumns: + - "IcaoCode" + - "Name" + - "IataCode" + orderBy: + dialect: odata + supportedColumns: + - "Name" + - "IcaoCode" + - "IataCode" + top: + dialect: odata + maxValue: 100 + count: + dialect: odata + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/airports/methods/list' + insert: [] + update: [] + replace: [] + delete: []